From e8544b19a766675abd0ac81751818827afee371b Mon Sep 17 00:00:00 2001 From: worker-dlmsettings Date: Mon, 9 Nov 2026 02:00:42 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(m193):=20=E4=B8=8B=E8=BD=BD=E5=B9=B6?= =?UTF-8?q?=E5=8F=91/=E9=87=8D=E8=AF=95/gh-proxy=20=E7=9C=9F=E6=8E=A5?= =?UTF-8?q?=E8=BF=9B=E5=BC=95=E6=93=8E=EF=BC=88schema=20=E4=B8=8E=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E5=90=8C=E6=BA=90=E5=B8=B8=E9=87=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - download.concurrency / download.retry_limit 以前引擎侧零消费者(恒用 EngineConfig::default() 的 64,而 schema 说 8)。现在默认值与硬上限收成 download/config.rs 的常量,schema、EngineConfig::default()、Semaphore 容量 三处同源;宿主把三个键翻译成 DownloadTuning 后经 shared::set_tuning(按数据根) 灌进装配点,改了设置下一次取引擎就生效。 - download.gh_proxy 三态(留空=跟随内置默认 / off=关 / 前缀=覆盖)下沉到 shared::build,所有走共享引擎的下载路径(内容库/版本清单/字体/安装)一致。 - 删掉 MirrorTable::set_gh_proxy 与 default_gh_proxy 两个改了没用的入口。 - 下载设置页把三条假演示值换成说语义的中文行(含三态说明与 1~64 硬上限)。 - 守卫:探针读 Semaphore 真实许可数(不是读配置),每条配变异证明。 - docs/download-settings.md 记下取证表(另四个键仍零消费,已立 finding)。 --- .../app-iced/src/pages/settings/download.rs | 27 +- .../pages/settings/tests/download_section.rs | 74 +++- crates/core/src/download/config.rs | 362 +++++++++++++++++- crates/core/src/download/engine.rs | 41 +- crates/core/src/download/mirrors.rs | 37 +- crates/core/src/download/shared.rs | 124 +++++- crates/core/src/setting/keys.rs | 36 +- crates/core/tests/download/mirrors_test.rs | 9 +- crates/presenter/src/host/dispatch.rs | 5 + crates/presenter/src/host/engine.rs | 12 +- crates/presenter/src/host/settings.rs | 89 +---- .../presenter/src/host/settings/download.rs | 174 +++++++++ crates/presenter/src/host/settings_tests.rs | 49 +++ docs/download-settings.md | 119 ++++++ docs/images/downloads-settings-zh.png | Bin 0 -> 95845 bytes 15 files changed, 958 insertions(+), 200 deletions(-) create mode 100644 crates/presenter/src/host/settings/download.rs create mode 100644 docs/download-settings.md create mode 100644 docs/images/downloads-settings-zh.png diff --git a/crates/app-iced/src/pages/settings/download.rs b/crates/app-iced/src/pages/settings/download.rs index b3d8a09..e064dae 100644 --- a/crates/app-iced/src/pages/settings/download.rs +++ b/crates/app-iced/src/pages/settings/download.rs @@ -1,9 +1,25 @@ -//! 设置页「Downloads」分组:首选源 / 并发数 / 校验(演示行)。 +//! 设置页「Downloads」分组:并发 / 单源尝试次数 / GitHub 加速前缀(M193)。 //! //! **这一组归 download 线**:接真控件时在本文件把 `body` 换成 `Some(download::body)`(渲染器也写在本文件), //! `settings/mod.rs` 的注册表不用动。 //! //! 注意与下载页(`pages/download*`)的区别:那是「装哪个版本」,这里是「怎么下」的默认值。 +//! +//! ## 为什么这三行是「说语义」而不是「说当前值」(M193) +//! +//! 原来这三行是**写死的演示值**(`("Concurrency", "8 threads")` / `("Preferred source", +//! "Automatic (region)")` / `("Verify checksums", "On")`)。用户实测把它们当成真值投诉过 +//! (「它默认显示 8,可我改成 2 也没变」)——因为 iced 侧**根本没有设置数据通路**: +//! `pages::Model` 没有设置快照、`grep -rn "SettingsChanged" crates/app-iced/src` 零命中、 +//! [`State`] 是个空 struct 且无人填。而这三行里「8 threads」当时还是**假的**:引擎真值曾是 64 +//! (schema 说 8),「Verify checksums」更是没有任何校验开关(校验恒开)。 +//! +//! 所以这一轮把假值换成**不会撒谎的语义行**:区间、默认值、「留空 / off / 前缀」三态各是什么。 +//! 真值(用户在 `launcher.json` 里填的那个数)不在这里显示——**宁可不说,也不说假的**。 +//! 真正的可改控件要等一条 presenter 快照 → app → `Groups.download` 的数据通路,另立 mission。 +//! +//! **生效保证**:这三条设置真的会被引擎读走(`core::download::shared::set_tuning`,宿主侧 +//! 翻译在 `presenter::host::settings`),所以「行里的语义」与「引擎行为」是同一份契约。 use unlucky_design::icons::IconKind; @@ -14,9 +30,11 @@ pub(super) const GROUP: Group = Group { name: "Downloads", icon: IconKind::Download, rows: &[ - ("Preferred source", "Automatic (region)"), - ("Concurrency", "8 threads"), - ("Verify checksums", "On"), + ("并发数", "出厂 8 · 可调 1~64"), + ("单源尝试次数", "出厂 3 · 含首次 · 换源另算"), + ("GitHub 加速前缀", "留空 = 跟随内置默认"), + ("关掉它", "填 off = 关闭"), + ("换掉它", "填 URL 前缀 = 覆盖"), ], body: None, }; @@ -24,6 +42,7 @@ pub(super) const GROUP: Group = Group { /// Downloads 分组的运行期状态(M101 r2:**只归 download 线**)。 /// /// 加字段只改本文件:`Groups` 只认 `Groups.download` 这一个字段。 +/// 现在仍然是空的——真值的数据通路还没接(见文件头「为什么这三行是说语义」)。 #[derive(Debug, Default)] pub struct State {} diff --git a/crates/app-iced/src/pages/settings/tests/download_section.rs b/crates/app-iced/src/pages/settings/tests/download_section.rs index f3ca8bc..46b3fc7 100644 --- a/crates/app-iced/src/pages/settings/tests/download_section.rs +++ b/crates/app-iced/src/pages/settings/tests/download_section.rs @@ -2,7 +2,7 @@ //! 一条 section 断言都没有;这 3 条是新增,不是搬移)。 //! //! **归 download 线**。接真控件那天要改**两个**文件(M101 r2 删掉了原先的第 3 处跨 section 绊线): -//! `settings/download.rs` + 本文件 `the_downloads_group_is_still_demo_rows`。 +//! `settings/download.rs` + 本文件。 //! //! 别把它和下载页(`pages/download*`)搞混:那是「装哪个版本」,这一组是「怎么下」的默认值。 @@ -11,18 +11,20 @@ use unlucky_design::icons::IconKind; /// 这一组的行集合(标签 + 值)。 /// -/// 抓得住:① 四条线并行合并时把别人的行粘到本文件(四份 `GROUP` 长得几乎一样,最容易整块贴错); -/// ② 漏掉/多出一条行;③ 标签与值错位(`("Concurrency", "8 threads")` 被写成 `"Off"`)。 -/// 抓不住:值对不对(`Automatic (region)` 是不是本机真选中的源)——那是接线后真数据源的事。 +/// 抓得住:① 四条线并行合并时把别人的行粘到本文件(六份 `GROUP` 长得几乎一样,最容易整块贴错); +/// ② 漏掉/多出一条行;③ 标签与值错位。 +/// 抓不住:值对不对——这三行**本来就不是值**(M193 起改成语义行,见下一条用例)。 #[test] -fn the_downloads_group_shows_source_concurrency_and_checksums() { +fn the_downloads_group_shows_concurrency_retries_and_the_gh_proxy_key() { let rows: Vec<(&str, &str)> = download::GROUP.rows.to_vec(); assert_eq!( rows, vec![ - ("Preferred source", "Automatic (region)"), - ("Concurrency", "8 threads"), - ("Verify checksums", "On"), + ("并发数", "出厂 8 · 可调 1~64"), + ("单源尝试次数", "出厂 3 · 含首次 · 换源另算"), + ("GitHub 加速前缀", "留空 = 跟随内置默认"), + ("关掉它", "填 off = 关闭"), + ("换掉它", "填 URL 前缀 = 覆盖"), ] ); } @@ -33,17 +35,59 @@ fn the_downloads_group_keeps_its_icon() { assert_eq!(download::GROUP.icon, IconKind::Download); } -/// **绊线**:这一组还没接真控件,画的是演示行。 +/// **M193 换了绊线的内容,但没撤绊线**。 /// -/// 抓得住:download 线接上了 `body` 却没更新这里的期望(那会让本文件静默变成假绿)。 -/// 抓不住:界面上真控件长什么样。 +/// 原绊线是 `the_downloads_group_is_still_demo_rows`:断言 `body.is_none()`(这一组还没接真控件)。 +/// 它存在的意义是「防止演示值回潮」。M193 撤掉的只是**演示值**(`("Concurrency", "8 threads")` +/// 这类):那个 8 与引擎真实的 64 对不上,用户按它调设置自然没效果。 /// -/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**(M101 r2 起不必再去 -/// `tests/mod.rs` 报到:那条跨 section 聚合绊线已删)。 +/// 现在改成两条更硬的断言: +/// +/// 1. **仍然没有真控件**(`body.is_none()`)——真值的通路还没接(`State` 是空 struct、 +/// iced 侧没有设置快照),所以这一组还是静态行;接上那天把这条改成 `is_some()` 并接数据。 +/// 2. **行里不许再出现「当前值」形态的字面量**:三行必须把「出厂值 / 区间 / 三态」讲清楚, +/// 且 gh-proxy 那一行必须**同时**说清「留空 = 跟随内置默认」与「填 off = 关闭」—— +/// 这两者最容易被混(一个「我没决定」,一个是「我明确不要」)。 +/// +/// 变异证明:把 `download.rs` 的 gh_proxy 行改回 `("Preferred source", "Automatic (region)")` +/// ⇒ 第 2 条红;把 `enabled` 语义写成「留空 = 关闭」⇒ 第 2 条红。 #[test] -fn the_downloads_group_is_still_demo_rows() { +fn the_downloads_rows_state_semantics_and_never_fake_a_current_value() { assert!( download::GROUP.body.is_none(), - "Downloads 还没接真控件:接了就改本断言" + "Downloads 还没接真控件:接了就改成 is_some() 并接数据通路" ); + + let joined = download::GROUP + .rows + .iter() + .map(|(label, value)| format!("{label} {value}")) + .collect::>() + .join(" | "); + + for required in [ + // 三态里最容易被混的两档,必须都在(缺一个就是「设置项说假话」的种子)。 + "留空 = 跟随内置默认", + "填 off = 关闭", + "填 URL 前缀 = 覆盖", + // 硬上限必须在界面上说清(任务书的边界条款)。 + "1~64", + // 三个键各自是哪一行(标签本身也是契约:用户按它认设置)。 + "并发数", + "单源尝试次数", + "GitHub 加速前缀", + ] { + assert!( + joined.contains(required), + "行里缺少「{required}」:{joined}" + ); + } + + // 「当前值」形态的假数据:`8 threads` / `On` / `Automatic (region)` 都是当年被投诉的那类。 + for forbidden in ["threads", "Automatic (region)", "Verify checksums"] { + assert!( + !joined.contains(forbidden), + "又出现假的当前值「{forbidden}」:{joined}" + ); + } } diff --git a/crates/core/src/download/config.rs b/crates/core/src/download/config.rs index 8fcf892..e384ca6 100644 --- a/crates/core/src/download/config.rs +++ b/crates/core/src/download/config.rs @@ -1,17 +1,51 @@ //! 引擎配置:设置层(core::setting)直接映射到这里。 +//! +//! # 出厂默认值与设置 schema 的唯一来源(M193) +//! +//! 四个常量是**唯一**写死数字的地方:schema 的默认值/范围、`EngineConfig::default()`、 +//! `Semaphore` 容量全从它们取。此前 schema 写 8、引擎写 64 ⇒ 调并发完全无效(引擎侧零消费者)。 +//! 收成常量后「改一处另一个不变」会直接变红(守卫见 `shared.rs` 与本文件的用例)。 +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; use std::time::Duration; use super::backoff::BackoffPolicy; use super::chunked::ChunkedConfig; +use super::region::RegionProfile; use super::types::ResumePolicy; +/// 全局并发许可数的出厂默认(`download.concurrency` 的 schema 默认值同源)。 +pub const DEFAULT_MAX_CONCURRENCY: usize = 8; + +/// 全局并发许可的**硬上限**(`download.concurrency` 的 schema 上界同源)。schema 在写入口就拒绝 +/// 越界值,这里再夹一次是纵深防御(手改文件、老 schema 允许的大数)——9999 会把机器拖死。 +pub const MAX_CONCURRENCY: usize = 64; + +/// 每个源尝试次数的出厂默认(`download.retry_limit` 的 schema 默认值同源)。 +pub const DEFAULT_MAX_ATTEMPTS_PER_SOURCE: u32 = 3; + +/// 每个源尝试次数的硬上限(`download.retry_limit` 的 schema 上界同源)。 +pub const MAX_ATTEMPTS_PER_SOURCE: u32 = 10; + +/// 三个设置键名(层间契约):登记在 `setting::keys`,读取在宿主。只写在这里,两侧都引用常量。 +pub const KEY_CONCURRENCY: &str = "download.concurrency"; +/// 见 [`KEY_CONCURRENCY`]。 +pub const KEY_RETRY_LIMIT: &str = "download.retry_limit"; +/// gh-proxy 前缀覆盖键(三态语义见 [`gh_proxy_override`])。 +/// +/// **暂未登记**:由 M197 在 `setting::keys::proxy` 登记,尚未合入 dev;宿主读它得到 `None` ⇒ +/// [`GhProxyOverride::Builtin`](行为正确)。**M197 合入后这条断言应升级为 schema 断言**——现在 +/// 只断言常量本身,别把这条弱断言当成可以随手删的。 +pub const KEY_GH_PROXY: &str = "download.gh_proxy"; + /// 引擎配置。 #[derive(Debug, Clone)] pub struct EngineConfig { - /// 全局并发许可数(tokio `Semaphore`),默认 64。 + /// 全局并发许可数(tokio `Semaphore`),默认 [`DEFAULT_MAX_CONCURRENCY`]。 pub max_concurrency: usize, - /// 每个源的尝试次数上限,默认 3。 + /// 每个源的尝试次数上限,默认 [`DEFAULT_MAX_ATTEMPTS_PER_SOURCE`]。 pub max_attempts_per_source: u32, /// 连接超时,默认 10s。 pub connect_timeout: Duration, @@ -32,8 +66,8 @@ pub struct EngineConfig { impl Default for EngineConfig { fn default() -> Self { Self { - max_concurrency: 64, - max_attempts_per_source: 3, + max_concurrency: DEFAULT_MAX_CONCURRENCY, + max_attempts_per_source: DEFAULT_MAX_ATTEMPTS_PER_SOURCE, connect_timeout: Duration::from_secs(10), read_timeout: Duration::from_secs(30), user_agent: concat!("UnluckyLauncher/", env!("CARGO_PKG_VERSION")).to_string(), @@ -44,3 +78,323 @@ impl Default for EngineConfig { } } } + +/// `download.gh_proxy` 的三种取值(三态语义见 [`gh_proxy_override`])。 +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum GhProxyOverride { + /// 空串:**没有覆盖**,跟随内置默认(地区判定 + 镜像表里的前缀)。 + #[default] + Builtin, + /// 明确关掉:一个 gh-proxy 候选都不生成(`RegionProfile::with_gh_proxy(None)`)。 + Off, + /// 用这个前缀覆盖(仍然只在镜像优先 / 国内判定时启用)。 + Prefix(String), +} + +/// 设置原文 → gh-proxy 三态(纯函数:`off` 与 URL 的边界在这里定死,不散落到宿主)。 +/// +/// **空串不等于关**:「我不想用加速」和「我还没决定」是两件事。空串 = 跟随内置默认, +/// 要关必须写 `off`(`none` / `direct` / `disable` / `disabled` / `no` 这些口语词一并认, +/// 大小写不敏感、前后空白忽略——用户拿哪个词表达「关掉」都合理,专门要求某一个会造出 +/// 「填了没生效」的坑)。 +#[must_use] +pub fn gh_proxy_override(raw: &str) -> GhProxyOverride { + let text = raw.trim(); + if text.is_empty() { + return GhProxyOverride::Builtin; + } + let lowered = text.to_ascii_lowercase(); + if matches!( + lowered.as_str(), + "off" | "none" | "direct" | "disable" | "disabled" | "no" + ) { + return GhProxyOverride::Off; + } + GhProxyOverride::Prefix(text.to_string()) +} + +/// 设置侧灌进来的下载调优(M193):并发 / 每个源的尝试次数 / gh-proxy 覆盖。 +/// +/// 这是「`launcher.json` 的三个键」到「引擎装配参数」之间**唯一**的翻译层:键名由宿主读(core +/// 不认识设置键名),默认值、合法区间、回落策略都在这里——否则「UI 说 8、引擎跑 64」会从缝里长回来。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DownloadTuning { + /// 全局并发许可数(已夹在 `1..=MAX_CONCURRENCY`)。 + pub max_concurrency: usize, + /// 每个源的尝试次数上限(已夹在 `1..=MAX_ATTEMPTS_PER_SOURCE`)。 + pub max_attempts_per_source: u32, + /// gh-proxy 覆盖。 + pub gh_proxy: GhProxyOverride, +} + +impl Default for DownloadTuning { + fn default() -> Self { + Self { + max_concurrency: DEFAULT_MAX_CONCURRENCY, + max_attempts_per_source: DEFAULT_MAX_ATTEMPTS_PER_SOURCE, + gh_proxy: GhProxyOverride::Builtin, + } + } +} + +impl DownloadTuning { + /// 设置原文 → 调优(**未填 / 越界 / 非法值一律回落出厂默认**,绝不报错阻断下载)。 + /// + /// 口径与 schema 的 `(min, max)` 同源但独立实现(用户可能手改文件绕过 `store.set`):键不存在 + /// → 出厂默认;落在区间内 → 原值;0 / 负数 / 超上限 → 出厂默认(**不夹到边界**:那是「想 9999 + /// 给 64」,界面仍会说谎)。 + #[must_use] + pub fn from_settings( + concurrency: Option, + attempts: Option, + gh_proxy: Option<&str>, + ) -> Self { + let default = Self::default(); + Self { + max_concurrency: concurrency + .and_then(|raw| usize::try_from(raw).ok()) + .filter(|raw| (1..=MAX_CONCURRENCY).contains(raw)) + .unwrap_or(default.max_concurrency), + max_attempts_per_source: attempts + .and_then(|raw| u32::try_from(raw).ok()) + .filter(|raw| (1..=MAX_ATTEMPTS_PER_SOURCE).contains(raw)) + .unwrap_or(default.max_attempts_per_source), + gh_proxy: gh_proxy.map_or(GhProxyOverride::Builtin, gh_proxy_override), + } + } + + /// 把调优盖到 `EngineConfig` 上(其余字段保持传入值)。 + #[must_use] + pub fn apply_to(&self, mut cfg: EngineConfig) -> EngineConfig { + cfg.max_concurrency = self.max_concurrency; + cfg.max_attempts_per_source = self.max_attempts_per_source; + cfg + } + + /// 把 gh-proxy 覆盖盖到档案上(`Builtin` 原样返回 = 跟随地区判定与镜像表)。 + #[must_use] + pub fn apply_gh_proxy(&self, profile: RegionProfile) -> RegionProfile { + match &self.gh_proxy { + GhProxyOverride::Builtin => profile, + GhProxyOverride::Off => profile.with_gh_proxy(None), + GhProxyOverride::Prefix(prefix) => profile.with_gh_proxy(Some(prefix)), + } + } +} + +/// 每个缓存根一份「宿主想要什么」(`engine_for` 构造时用它,见 [`wanted`])。按数据根而不是进程级: +/// 登记处本就是「一个数据根一个引擎」;进程级那份在多数据根时会互相踩(A 根改 2、B 根冲回 8)。 +fn wanted_slot() -> &'static Mutex> { + static WANTED: OnceLock>> = OnceLock::new(); + WANTED.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// 该数据根当前想要的调优(没灌过 = 出厂默认)。 +#[must_use] +pub fn wanted(cache_root: &Path) -> DownloadTuning { + wanted_slot() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .get(cache_root) + .cloned() + .unwrap_or_default() +} + +/// 记下该数据根想要的调优,返回**是否真的变了**(没变时调用方不该重建引擎)。 +pub fn publish(cache_root: &Path, next: DownloadTuning) -> bool { + let mut wanted = wanted_slot() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // 归一化比较:「没灌过」与「灌了出厂默认」是同一件事,不能因为 `None != Some(default)` + // 就把每次取引擎都当成「参数变了」(那会每次重建引擎、把连接池与健康度记忆全丢掉)。 + let current = wanted.get(cache_root).cloned().unwrap_or_default(); + if current == next { + return false; + } + if next == DownloadTuning::default() { + // 默认值不占格子(也顺手回收测试造的临时根)。 + wanted.remove(cache_root); + } else { + wanted.insert(cache_root.to_path_buf(), next); + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::setting::keys::{launcher_schema, launcher_version}; + + /// **守门③**:schema 的默认值与 `EngineConfig::default()` / 调优默认值是同一个常量。 + /// + /// 变异证明(只改实现侧):把 `EngineConfig::default()` 的 `max_concurrency` 写回字面量 64 + /// ⇒ 本用例红在 `max_concurrency`;把 `setting/keys.rs` 的 `download.concurrency` 默认值写回 + /// 字面量 8 而常量改成 16 ⇒ 红在 schema 一侧。这条防的正是「两处各写一个数」长回来 + /// (8 vs 64 就是这么长出来的)。 + #[test] + fn the_schema_defaults_are_the_engine_defaults() { + let schema = launcher_schema(); + assert_eq!(schema.version(), launcher_version()); + let cfg = EngineConfig::default(); + let tune = DownloadTuning::default(); + + // 键名常量与 schema 登记是同一份(宿主按常量读,schema 按常量登记)。 + assert!(schema.spec(KEY_CONCURRENCY).is_some(), "{KEY_CONCURRENCY}"); + assert!(schema.spec(KEY_RETRY_LIMIT).is_some(), "{KEY_RETRY_LIMIT}"); + + let concurrency = schema.spec(KEY_CONCURRENCY).expect("键已登记"); + assert_eq!( + concurrency.default, + serde_json::json!(DEFAULT_MAX_CONCURRENCY) + ); + assert_eq!(cfg.max_concurrency, DEFAULT_MAX_CONCURRENCY); + assert_eq!(tune.max_concurrency, DEFAULT_MAX_CONCURRENCY); + + let retry = schema.spec(KEY_RETRY_LIMIT).expect("键已登记"); + assert_eq!( + retry.default, + serde_json::json!(DEFAULT_MAX_ATTEMPTS_PER_SOURCE) + ); + assert_eq!(cfg.max_attempts_per_source, DEFAULT_MAX_ATTEMPTS_PER_SOURCE); + assert_eq!( + tune.max_attempts_per_source, + DEFAULT_MAX_ATTEMPTS_PER_SOURCE + ); + } + + /// 硬上限:schema 的上界与引擎的夹取上限是同一个常量(防「schema 收 256、引擎不知情」)。 + /// 变异证明:只改 `MAX_CONCURRENCY` 而 schema 仍写 64 ⇒ 红。 + #[test] + fn the_hard_caps_are_single_sourced() { + let schema = launcher_schema(); + let concurrency = i64::try_from(MAX_CONCURRENCY).expect("装得下"); + let attempts = i64::from(MAX_ATTEMPTS_PER_SOURCE); + for (key, max) in [(KEY_CONCURRENCY, concurrency), (KEY_RETRY_LIMIT, attempts)] { + let spec = schema.spec(key).expect("键已登记"); + assert_eq!( + spec.kind, + crate::setting::spec::ValueKind::Int { min: 1, max } + ); + } + } + + /// 未填 / 非法值回落默认,不阻断(任务书的边界条款)。 + /// + /// 变异证明:把 `filter(|raw| (1..=MAX_CONCURRENCY).contains(raw))` 删掉 ⇒ `9999` 与 `0` + /// 两处断言红;把 `unwrap_or(default.max_concurrency)` 改成 `unwrap_or(1)` ⇒ `None` 断言红。 + #[test] + fn missing_or_illegal_values_fall_back_to_the_defaults() { + let tune = DownloadTuning::from_settings(None, None, None); + assert_eq!(tune, DownloadTuning::default()); + + let just_over = i64::try_from(MAX_CONCURRENCY).expect("上限装得下") + 1; + for bad in [0_i64, -1, 9_999, just_over] { + let tune = DownloadTuning::from_settings(Some(bad), Some(bad), None); + assert_eq!( + tune, + DownloadTuning::default(), + "{bad} 越界时应回落默认而不是夹到边界" + ); + } + + let tune = DownloadTuning::from_settings(Some(2), Some(1), None); + assert_eq!(tune.max_concurrency, 2); + assert_eq!(tune.max_attempts_per_source, 1); + } + + /// 三态:**空串 ≠ 关**(这条是「设置项说假话」的高发点,M197 的键语义同源)。 + #[test] + fn empty_gh_proxy_means_follow_builtin_not_off() { + assert_eq!(gh_proxy_override(""), GhProxyOverride::Builtin); + assert_eq!(gh_proxy_override(" "), GhProxyOverride::Builtin); + for off in [ + "off", "OFF", " none ", "direct", "disable", "disabled", "no", + ] { + assert_eq!(gh_proxy_override(off), GhProxyOverride::Off, "{off}"); + } + assert_eq!( + gh_proxy_override(" https://gh.example/ "), + GhProxyOverride::Prefix("https://gh.example/".to_string()) + ); + // 未填(键不存在)也是 Builtin,不是 Off。 + assert_eq!( + DownloadTuning::from_settings(None, None, None).gh_proxy, + GhProxyOverride::Builtin + ); + } + + /// `apply_gh_proxy` 的三档真的改到档案上、并且**改到候选链上**(`MirrorTable::expand()` 读的就是 + /// 档案里那个前缀,内容库/版本清单都走它)。这是「改键 ⇒ 候选链变」的 core 级一半。 + /// 变异证明:把 `Off` 分支改成原样返回 `profile` ⇒ `off` 那条红。 + #[test] + fn gh_proxy_override_reaches_the_region_profile_and_the_candidate_chain() { + use crate::download::mirrors::{MirrorTable, SOURCE_GH_PROXY}; + use crate::download::types::ResourceKind; + + let url = "https://github.com/o/r/releases/download/v1/a.jar"; + let table = MirrorTable::builtin(); + let chain = |profile: &RegionProfile| -> Vec { + table + .expand(url, ResourceKind::Other, profile) + .into_iter() + .map(|candidate| candidate.url) + .collect() + }; + + let mirror_first = RegionProfile::mirror_first(0); + let kept = DownloadTuning::default().apply_gh_proxy(mirror_first.clone()); + assert_eq!(kept.gh_proxy, mirror_first.gh_proxy, "Builtin 不该动档案"); + assert!( + chain(&kept).iter().any(|url| url.contains("gh-proxy")), + "出厂默认会套前缀" + ); + + let off = DownloadTuning::from_settings(None, None, Some("off")) + .apply_gh_proxy(mirror_first.clone()); + assert!(!off.gh_proxy_enabled(), "off 必须关掉 gh-proxy"); + assert_eq!(off.gh_proxy, None); + assert!( + !chain(&off).iter().any(|url| url.contains("gh-proxy")), + "关了就不该有候选" + ); + assert!( + table + .expand(url, ResourceKind::Other, &off) + .iter() + .all(|candidate| candidate.source != SOURCE_GH_PROXY), + "连 source 也不该出现" + ); + + let prefix = DownloadTuning::from_settings(None, None, Some("https://gh.example/")) + .apply_gh_proxy(mirror_first); + assert_eq!(prefix.gh_proxy.as_deref(), Some("https://gh.example/")); + assert!(prefix.gh_proxy_enabled()); + assert!( + chain(&prefix) + .iter() + .any(|url| url.starts_with("https://gh.example/")), + "覆盖前缀必须进候选链" + ); + } + + /// 每个数据根的调优互不串味(一个根改成 2 不该改另一个根的引擎)。 + /// 变异证明:把 `wanted`/`publish` 的键改回单一进程级槽位 ⇒ 第二个根读到 2,红。 + #[test] + fn tuning_is_per_cache_root_and_no_change_is_reported_as_such() { + let a = PathBuf::from("/tmp/ull-tuning-a"); + let b = PathBuf::from("/tmp/ull-tuning-b"); + assert_eq!(wanted(&a), DownloadTuning::default()); + + let changed = DownloadTuning::from_settings(Some(2), None, None); + assert!(publish(&a, changed.clone()), "真变了要报变更"); + assert_eq!(wanted(&a), changed); + assert!(!publish(&a, changed.clone()), "同值不该报变更"); + assert_eq!(wanted(&b), DownloadTuning::default(), "别的根不受影响"); + + assert!( + publish(&a, DownloadTuning::default()), + "恢复默认也是一次变更" + ); + assert_eq!(wanted(&a), DownloadTuning::default()); + } +} diff --git a/crates/core/src/download/engine.rs b/crates/core/src/download/engine.rs index 091521e..0063bb1 100644 --- a/crates/core/src/download/engine.rs +++ b/crates/core/src/download/engine.rs @@ -5,18 +5,15 @@ //! ```text //! 校验请求 → 内容寻址缓存直取(零网络) //! → 候选规划(镜像表展开 + 健康度排序) -//! → Semaphore 限流 +//! → Semaphore 限流 ← 容量来自设置(M193,见 config.rs) //! → 候选源外循环(换源),每源内循环(指数退避 + 抖动) //! → 续传判定(保守)→ 单连接 / 多连接分块 → 流式校验 → 原子落盘 //! → 失败则保留中间文件供下次续传 //! ``` //! -//! 最后一步保留的是**本会话命名的**中间文件(`.ullpart--`,不再固定 -//! 叫 `.ullpart`):下次(甚至下次启动、重启后再下同一个文件)由 `PartFile::for_dest` -//! 在目标目录里认领它并改名成新的会话名,续传判定照旧。详细身份规则见 `atomic.rs` 模块头的 -//! 「中间文件的身份」。 -//! -//! 具体传输细节在 `transfer.rs`,错误分类在 `http.rs`,本文件只负责顺序与策略。 +//! 中间文件是本会话命名的(`.ullpart--`),下次(甚至下次启动)由 +//! `PartFile::for_dest` 认领并改名,续传判定照旧(身份规则见 `atomic.rs` 模块头)。传输细节在 +//! `transfer.rs`,错误分类在 `http.rs`,本文件只负责顺序与策略。 use std::path::Path; use std::sync::Arc; @@ -27,7 +24,7 @@ use tokio::sync::Semaphore; use super::atomic::PartFile; use super::backoff::Jitter; use super::cache::{now_ms, ContentCache}; -use super::config::EngineConfig; +use super::config::{EngineConfig, MAX_CONCURRENCY}; use super::error::{DownloadError, DownloadErrorKind}; use super::health::HealthRegistry; use super::http; @@ -72,8 +69,10 @@ impl DownloadEngine { cache: Arc, ) -> Self { let http = http::build_client(&cfg).unwrap_or_else(|_| reqwest::Client::new()); - let sem = Arc::new(Semaphore::new(cfg.max_concurrency.max(1))); - let seed = now_ms() ^ (u64::from(cfg.max_concurrency as u32) << 32); + // 硬上限夹取(M193):手改的 launcher.json / CLI 自拼的 cfg 都可能写着 9999,在唯一构造点再夹一次。 + let permits = cfg.max_concurrency.clamp(1, MAX_CONCURRENCY); + let sem = Arc::new(Semaphore::new(permits)); + let seed = now_ms() ^ (u64::from(u32::try_from(permits).unwrap_or(u32::MAX)) << 32); // 纯记账(M165):卡在构造这个点上,才统计得到绕过登记处的调用点(守卫靠 `constructions`)。 super::shared::record_build(cache.root()); Self { @@ -102,10 +101,15 @@ impl DownloadEngine { &self.cfg } - /// HTTP 客户端(同一条连接池与超时配置)。 - /// - /// 给「一锤子查询」用——例如 Adoptium 的版本查询(`core::game::java::api`): - /// 没必要为了一个 GET 再造一个 client,代理/超时/证书栈都在这里统一。 + /// **真实并发闸门**此刻的许可数(探针,M193):读 [`Self::acquire`] 会去抢的那张 + /// `Semaphore`,**不是** `config().max_concurrency`。有下载在飞时偏小,空闲等于容量。 + #[must_use] + pub fn concurrency_probe(&self) -> usize { + self.sem.available_permits() + } + + /// HTTP 客户端(同一条连接池与超时配置):给「一锤子查询」用(如 Adoptium 版本查询), + /// 没必要为了一个 GET 再造一个 client。 #[must_use] pub fn client(&self) -> &reqwest::Client { &self.http @@ -243,7 +247,7 @@ impl DownloadEngine { } Err(err) => { // 4xx(NotFound)不算「源坏」:院校镜像只留最新补丁,请求旧补丁必然 404, - // 记成失败会把唯一可用镜像冷却掉(M33/M34 实测:TUNA 每个目录只保 1 个归档)。 + // 记成失败会把唯一可用镜像冷却掉(M33/M34 实测:TUNA 每目录只保 1 个归档)。 if err.kind == DownloadErrorKind::NotFound { self.health.record_missing( &candidate.source, @@ -346,11 +350,8 @@ impl DownloadEngine { }) } - /// 该源此刻给几次尝试: - /// - /// * 冷却中 → 1 次(这次就是「恢复探测」,失败会重新冷却); - /// * 刚出冷却、还没洗白 → 也只给 1 次(`is_probe()`,别让死源每次到期都吃满 3 次); - /// * 正常 → 配置的 `max_attempts_per_source`。 + /// 该源此刻给几次尝试:冷却中 / 刚出冷却未洗白 → 1 次(那就是恢复探测,别让死源每次到期 + /// 都吃满预算);正常 → 设置里的 `download.retry_limit`(= `max_attempts_per_source`)。 pub(crate) fn attempts_for(&self, source: &str, kind: ResourceKind, now_ms: u64) -> u32 { let health = self.health.health_of(source, kind); if health.is_cooling_down(now_ms) || health.is_probe(now_ms) { diff --git a/crates/core/src/download/mirrors.rs b/crates/core/src/download/mirrors.rs index e16b6cb..e48e3bc 100644 --- a/crates/core/src/download/mirrors.rs +++ b/crates/core/src/download/mirrors.rs @@ -175,49 +175,18 @@ impl MirrorTable { self.rules.insert(0, rule); } - /// 覆盖 gh-proxy 前缀(设置项);传 `None` 表示不默认启用。 - pub fn set_gh_proxy(&mut self, prefix: Option<&str>) { - match prefix { - Some(prefix) => { - if let Some(proxy) = self.gh_proxies.first_mut() { - proxy.prefix = prefix.to_string(); - proxy.enabled = true; - } else { - self.gh_proxies.push(GhProxy { - id: "gh-proxy".to_string(), - name: "自定义 gh-proxy".to_string(), - prefix: prefix.to_string(), - enabled: true, - }); - } - } - None => { - for proxy in &mut self.gh_proxies { - proxy.enabled = false; - } - } - } - } - /// 找到第一条命中的前缀规则。 #[must_use] pub fn prefix_rule_for(&self, url: &str, kind: ResourceKind) -> Option<&PrefixRule> { self.rules.iter().find(|rule| rule.matches(url, kind)) } - /// 清单里第一个启用的 gh-proxy 前缀(默认值来源;设置层用它初始化地区档案)。 - #[must_use] - pub fn default_gh_proxy(&self) -> Option<&str> { - self.gh_proxies - .iter() - .find(|proxy| proxy.enabled && !proxy.prefix.is_empty()) - .map(|proxy| proxy.prefix.as_str()) - } - /// GitHub host 且允许时套 gh-proxy 前缀。 /// /// **生效前缀的唯一真相源是 `region.gh_proxy`**(设置层/地区判定写入,`None` = 用户关闭)。 - /// 表格里的 `gh_proxies` 只是「已知可用前缀」清单,配合 `default_gh_proxy()` 给设置做默认值。 + /// 表格里的 `gh_proxies` 只是「已知可用前缀」清单(诊断页可读),**改它不会生效**——M193 + /// 删掉了原先那个 `MirrorTable::set_gh_proxy`,正是因为它改了这张表而 `expand()` 读的是档案, + /// 属于「看起来能改、其实没人读」的入口。 #[must_use] pub fn gh_proxy_candidates(&self, url: &str, region: &RegionProfile) -> Vec { if region.mode != RegionMode::MirrorFirst { diff --git a/crates/core/src/download/shared.rs b/crates/core/src/download/shared.rs index be5231d..f3a59b6 100644 --- a/crates/core/src/download/shared.rs +++ b/crates/core/src/download/shared.rs @@ -32,23 +32,23 @@ //! 只 forget 的话它会一直用旧实例(含旧地区档案)直到进程结束。所以「重新检测地区」那条 //! 设置动作除了 [`forget`] 还调 `CoreHost::refresh_update_factory()` 把新引擎接过去 //! (M165 复审 P2-1:这是本模块文档原先说漏的一半)。 -//! * **并发多少**:`EngineConfig::default().max_concurrency`(64)现在是**进程级**上限, -//! 不再是「每请求 64」。单请求独占时与从前一致;多请求并发时它们分摊这 64 张许可。 +//! * **并发多少**(M193):容量来自设置 `download.concurrency`(出厂 8、硬上限 64,见 [`set_tuning`]), +//! 而且是**进程级**上限——多请求并发时分摊这批许可,不是「每请求 8」。 //! * **镜像切换 / 失效**:一律走引擎原有的候选规划 + 健康度冷却,本模块不参与判定;唯一可见的 //! 差别是健康度记忆现在真的跨请求累积(这本就是它存在的理由)。 //! //! # 与 [`super::blocking::block_on`] 的绑定关系 //! //! `reqwest` 的连接池绑在**驱动连接的运行时**上:引擎复用了而每次 `block_on` 都新起一个运行时, -//! 池里的空闲连接就属于已经销毁的运行时,复用会退化成「拿到的都是死连接」。所以引擎复用必须 -//! 配运行时常驻(`blocking` 模块那头一并改成进程内唯一运行时)。两者是同一件事的两半。 +//! 池里的空闲连接就属于已销毁的运行时,复用会退化成「拿到的都是死连接」。所以引擎复用必须配 +//! 运行时常驻(`blocking` 那一头一并改成进程内唯一运行时)。两者是同一件事的两半。 use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use super::cache::{now_ms, ContentCache}; -use super::config::EngineConfig; +use super::config::{publish, wanted, DownloadTuning, EngineConfig}; use super::engine::DownloadEngine; use super::health::HealthRegistry; use super::mirrors::MirrorTable; @@ -102,9 +102,8 @@ pub fn engine_for(cache_root: impl AsRef) -> Arc { /// 让该数据根的共享引擎失效(下次 [`engine_for`] 重新构造)。 /// /// **只让登记处不再持有那一份**:已经拿到 `Arc` 的人继续用旧的。短命请求自然结束;长命持有者 -/// (presenter 的自更新 `Factory`)必须自己重建,否则它会一直用旧实例——本仓在「重新检测地区」 -/// 的设置动作里调 `CoreHost::refresh_update_factory()` 补上这一步(见模块文档「`forget` 到什么范围」)。 -/// 返回是否真淘汰了一份。 +/// (presenter 的自更新 `Factory`)必须自己重建——「重新检测地区」那条设置动作就补调了 +/// `CoreHost::refresh_update_factory()`。返回是否真淘汰了一份。 pub fn forget(cache_root: impl AsRef) -> bool { registry() .lock() @@ -116,9 +115,7 @@ pub fn forget(cache_root: impl AsRef) -> bool { /// 记一笔「这个缓存根上又造了一个引擎」。 /// /// **唯一调用点是 [`DownloadEngine::new`]**(不是 [`engine_for`]):记账卡在构造这个卡口上, -/// 于是 [`constructions`] 统计的是**进程里真实发生过几次构造**,与「谁造的」无关。 -/// 若只在 `engine_for` 里记账,任何绕过登记处、自己 `DownloadEngine::new` 的调用点都统计不到 -/// (M165 复审备注 2 的诉求)。 +/// [`constructions`] 才是「进程里真实构造过几次」,与谁造的无关(M165 复审备注 2)。 pub(crate) fn record_build(cache_root: &Path) { *ledger() .lock() @@ -127,10 +124,8 @@ pub(crate) fn record_build(cache_root: &Path) { .or_default() += 1; } -/// 该数据根在本进程里**实际构造过几次引擎**(不论谁构造的)。 -/// -/// 这是给守卫与诊断用的:跨请求复用成立时,同一个根无论被请求多少次,这个数都停在 `1` -/// (`forget` 之后每重建一次 +1)。只断言「结果一样」测不出复用——引擎建几次都能下同一个文件。 +/// 该数据根在本进程里**实际构造过几次引擎**(不论谁构造的):复用成立时停在 `1`(`forget` +/// 之后每重建一次 +1)。只断言「结果一样」测不出复用——引擎建几次都能下同一个文件。 #[must_use] pub fn constructions(cache_root: impl AsRef) -> usize { ledger() @@ -141,9 +136,14 @@ pub fn constructions(cache_root: impl AsRef) -> usize { .unwrap_or(0) } -/// 真实构造(**唯一**一处 `DownloadEngine::new` 的宿主侧装配)。 +/// 真实构造(**唯一**一处 `DownloadEngine::new` 的宿主侧装配)。装配参数从该数据根的 +/// [`wanted`] 取,不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore` 容量、尝试次数 → +/// `attempts_for()` 上限、gh_proxy → 地区档案(`MirrorTable::expand()` 只认档案里那个前缀,所以 +/// 内容库 / 版本清单 / 字体 / 安装**所有**路径一起生效)。值由宿主翻译后经 [`set_tuning`] 灌入—— +/// core 不认识设置键名,也不依赖 `core::setting`。 fn build(cache_root: &Path) -> DownloadEngine { let now = now_ms(); + let tune = wanted(cache_root); let region_path = cache_root.join("region.json"); let region = load_cached(®ion_path, now).unwrap_or_else(|| { let profile = decide_from(&detect_signals(), now); @@ -151,14 +151,30 @@ fn build(cache_root: &Path) -> DownloadEngine { profile }); DownloadEngine::new( - EngineConfig::default(), + tune.apply_to(EngineConfig::default()), Arc::new(MirrorTable::builtin()), - Arc::new(region), + Arc::new(tune.apply_gh_proxy(region)), Arc::new(HealthRegistry::open_in(cache_root, now)), Arc::new(ContentCache::open(cache_root)), ) } +/// 换该数据根的下载调优(宿主取引擎前 / 设置变更时调用),返回**是否真的变了**。变了就清掉该根的 +/// 登记项,下次 [`engine_for`] 按新参数重建——否则「改了设置要重启才生效」。已拿到 `Arc` 的长命 +/// 持有者继续用旧的(与 [`forget`] 同一条边界);自更新 `Factory` 由 dispatch 侧换引擎。 +/// **锁序**:`publish` 的锁先释放再拿登记处的锁,[`build`] 反之——两段不嵌套。 +pub fn set_tuning(cache_root: impl AsRef, next: DownloadTuning) -> bool { + let key = key_of(cache_root.as_ref()); + if !publish(&key, next) { + return false; + } + let _ = registry() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .remove(&key); + true +} + #[cfg(test)] mod tests { use super::*; @@ -308,4 +324,76 @@ mod tests { assert_eq!(conns, 1, "两次下载应复用同一条连接"); std::fs::remove_dir_all(&dir).ok(); } + + /// **守门①②**:设置说 2 ⇒ 引擎**真实的并发闸门**就是 2(探针读 `Semaphore`,不是读配置); + /// 同一口装配点上的 gh-proxy 三档也真的改变 `plan()` 的候选链。 + /// + /// 变异证明(只改实现侧):① `build` 的 `tune.apply_to(...)` 换回裸 `EngineConfig::default()` + /// ⇒ 探针 8,红;② `tune.apply_gh_proxy(region)` 换回 `region` ⇒ `off` 档仍有候选,红; + /// ③ 删掉 `set_tuning` 里那条 `remove` ⇒ 换参数后交回旧引擎(探针 2 ≠ 5),红。 + /// gh-proxy 的**端到端**一半(schema 键 → 调优)要等 M197 登记该键,这里只守「调优 → 候选链」。 + #[test] + fn the_setting_and_the_gh_proxy_override_reach_the_engine() { + use super::super::types::{DownloadRequest, ResourceKind}; + use super::super::RegionProfile; + + let dir = root("tune"); + // 镜像优先的档案:gh-proxy 只在这个模式下才生成候选。 + store_cached( + &dir.join("region.json"), + &RegionProfile::mirror_first(now_ms()), + ) + .expect("地区档案"); + let req = DownloadRequest::new( + ResourceKind::Other, + "https://github.com/o/r/releases/download/v1/a.jar", + dir.join("a.jar"), + ); + let chain = |dir: &Path| -> Vec { + engine_for(dir) + .plan(&req, 0) + .into_iter() + .map(|c| c.url) + .collect() + }; + let set = |c: Option, a: Option, g: &str| { + set_tuning(&dir, DownloadTuning::from_settings(c, a, Some(g))) + }; + + set(Some(2), Some(1), "off"); + let engine = engine_for(&dir); + assert_eq!(engine.concurrency_probe(), 2, "闸门必须真是 2 张许可"); + assert_eq!( + engine.config().max_attempts_per_source, + 1, + "尝试次数同一条路" + ); + let off = chain(&dir); + assert!(!off.iter().any(|url| url.contains("gh-proxy")), "{off:?}"); + + set(Some(5), None, "https://gh.example/"); + let rebuilt = engine_for(&dir); + assert!(!Arc::ptr_eq(&engine, &rebuilt), "参数变了必须换引擎"); + assert_eq!(rebuilt.concurrency_probe(), 5, "新闸门吃新值"); + let over = chain(&dir); + assert!( + over.iter() + .any(|url| url.starts_with("https://gh.example/")), + "覆盖前缀必须出现在候选链里:{over:?}" + ); + + // 硬上限(纵深防御):宿主绕开 `from_settings` 直接塞 9999,闸门也只能开到 64。 + let wild = DownloadTuning { + max_concurrency: 9_999, + ..DownloadTuning::default() + }; + set_tuning(&dir, wild); + assert_eq!( + engine_for(&dir).concurrency_probe(), + super::super::config::MAX_CONCURRENCY, + "9999 张许可不能真开" + ); + + std::fs::remove_dir_all(&dir).ok(); + } } diff --git a/crates/core/src/setting/keys.rs b/crates/core/src/setting/keys.rs index ada28a0..687e3ba 100644 --- a/crates/core/src/setting/keys.rs +++ b/crates/core/src/setting/keys.rs @@ -5,6 +5,9 @@ use super::catalog::SettingSchema; use super::schema::{SchemaId, SchemaVersion}; use super::spec::{AiPolicy, SettingSpec}; +// M193:下载两键的默认值与硬上限的唯一来源在 `download::config`(引擎侧),这里只登记成 schema。 +use crate::download::config as download; + /// `core.data_mode` 允许值。 pub const DATA_MODES: &[&str] = &["portable", "standard", "custom"]; /// `download.region` 允许值。 @@ -45,23 +48,18 @@ pub mod update; /// /// `1.1.0` 登记 D20 实例隔离键(`instances.mode` 等)。 /// `1.2.0` 登记 D23 的 JDK 落点与发行版优先级键(`java.installRoot` / `java.vendorPriority`): -/// 同样只推 minor、不强制迁移——[`super::document`] 的「默认值不落盘」让老文件缺键即取默认, -/// 迁移链(按 major 走)不需要新步骤;`1.1.0 → 1.2.0` 的差异只体现在文件里出现新键之后。 +/// 只推 minor、不强制迁移——[`super::document`] 的「默认值不落盘」让老文件缺键即取默认。 /// `1.3.0` 登记 D17 的「外观(高级)」P1 键(`appearance.bg_image*` / `panel_alpha` / `glass` / -/// `accent_color`):同一套纯增量语义——出厂全默认(高级特性全关)即等于老文件缺键。 -/// `1.4.0` 登记 P4 的外观下层三键(`window_opacity` / `titlebar_mode` / `palette_source`,M91): -/// 仍然只推 minor、不加迁移步骤——三者的出厂值正是「老文件没有这些键」时的等价物。 -/// `1.5.0` 登记 M116 的窗口圆角策略(`appearance.window_corner`,出厂 `system`):纯增量。 -/// `1.6.0` 登记 M161 的「自更新」两键(见 [`update`]):纯增量但带一步数据迁移 -/// (此前住在 `cache/update/prefs.json`,迁移见 [`super::update_prefs`])。 +/// `accent_color`)与 `1.5.0` 的窗口圆角、`1.4.0` 的外观下层三键(M91):都是纯增量—— +/// 出厂全默认即等于老文件缺键。 +/// `1.6.0` 登记 M161 的「自更新」两键(见 [`update`]):纯增量 + 一步迁移(见 [`super::update_prefs`])。 /// `1.7.0` 登记 M168 的中文字体下载地址覆盖键(`ui.font_cdn`,见 [`font`]):纯增量、无迁移。 /// /// **旧读新(方向相反,措辞必须精确)**:1.4.0 的客户端读 1.5.0 的文件时, /// [`super::schema::SchemaCompat::check_url`] 走 `found.minor > expected.minor` → -/// `ReadOnlyPreserveSchema` → [`super::schema::SchemaCompat::access`] → `FileAccess::ReadOnly`。 -/// 也就是说**整份配置对它只读**(值照读、`store.set` 一律 `SettingError::ReadOnly`、`save` 跳过), -/// 不是「只是少一档」。这是 minor 提升的既定代价——也正是新键必须推 minor 而不是偷偷塞进旧版本的 -/// 原因:回退到旧版本会出现「设置改不动」,而原因只在 `$schema` 那一行里,不看版本号无从排查。 +/// `ReadOnlyPreserveSchema` → `FileAccess::ReadOnly`:**整份配置对它只读**(值照读、`store.set` +/// 一律 `SettingError::ReadOnly`),不是「只是少一档」。这正是新键必须推 minor 的原因—— +/// 回退到旧版本会出现「设置改不动」,而原因只在 `$schema` 那一行里。 #[must_use] pub fn launcher_version() -> SchemaVersion { SchemaVersion::new(1, 7, 0) @@ -102,19 +100,21 @@ pub fn launcher_schema() -> SettingSchema { AiPolicy::sensitive(), "实例默认隔离模式:isolated(运行目录派生 versions/<版本 id>,D20 默认)/ shared(所有实例共用 .minecraft 根)", )) + // M193:默认值/区间只有一个来源——`download::config` 的常量。此前这里写 8、 + // `EngineConfig::default()` 写 64,界面说「当前 8」引擎却跑 64,调并发完全无效。 .with(SettingSpec::int( "download.concurrency", - 8, - (1, 256), + download::DEFAULT_MAX_CONCURRENCY as i64, + (1, download::MAX_CONCURRENCY as i64), AiPolicy::silent(), - "下载并发数", + "同时下载的文件数(1~64,出厂 8)。这是引擎真实持有的并发许可,不是展示值;越界或手改坏值按出厂值生效", )) .with(SettingSpec::int( "download.retry_limit", - 3, - (0, 10), + download::DEFAULT_MAX_ATTEMPTS_PER_SOURCE as i64, + (1, download::MAX_ATTEMPTS_PER_SOURCE as i64), AiPolicy::silent(), - "单个文件重试次数", + "单个下载源的最多尝试次数(含首次,1 = 不重试;出厂 3)。换源不受此限:候选链上有几个源就各给几次", )) .with(SettingSpec::enumeration( "download.region", diff --git a/crates/core/tests/download/mirrors_test.rs b/crates/core/tests/download/mirrors_test.rs index 61c0696..c5eceeb 100644 --- a/crates/core/tests/download/mirrors_test.rs +++ b/crates/core/tests/download/mirrors_test.rs @@ -122,8 +122,13 @@ fn pinned_or_disabled_gh_proxy_is_respected() { .expect("自定义前缀未生效"); assert_eq!(proxy.url, format!("https://proxy.example/{GITHUB_URL}")); - // 清单给设置做默认值。 - assert_eq!(table.default_gh_proxy(), Some("https://gh-proxy.com/")); + // 「默认前缀」的唯一真相源是**地区档案**(M193:`MirrorTable::set_gh_proxy` / + // `default_gh_proxy` 已删除——它们改/读的是表里的前缀目录,而 `expand()` 读档案, + // 属于「看起来能改、其实没人读」。这条断言从此钉在档案上)。 + assert_eq!( + RegionProfile::mirror_first(0).gh_proxy.as_deref(), + Some(unlucky_core::download::region::DEFAULT_GH_PROXY) + ); } #[test] diff --git a/crates/presenter/src/host/dispatch.rs b/crates/presenter/src/host/dispatch.rs index 1bfc4e0..ccdf8e9 100644 --- a/crates/presenter/src/host/dispatch.rs +++ b/crates/presenter/src/host/dispatch.rs @@ -226,6 +226,11 @@ impl CoreHost { if key == super::engine::DETECT_REGION_ACTION { self.refresh_update_factory(); } + // M193:下载三键(并发/重试/gh-proxy)改了要立刻换引擎参数。这是**显式**的同步点 + // ——不藏在任何读函数里。值没变时它一行不动。 + if settings::download::apply_tuning(&self.env) { + self.refresh_update_factory(); + } self.send(CoreEvent::SettingsChanged(settings::groups(&self.env))); self.java_chain.clear(); self.refresh_java(); diff --git a/crates/presenter/src/host/engine.rs b/crates/presenter/src/host/engine.rs index 3dc1e04..13ab99f 100644 --- a/crates/presenter/src/host/engine.rs +++ b/crates/presenter/src/host/engine.rs @@ -22,8 +22,14 @@ use super::env::HostEnv; /// 取该数据根上**共享**的下载引擎(缓存/健康度/地区判定都在数据根里)。 /// /// 同一个数据根无论被请求多少次,都是同一个引擎;不同数据根各一份(缓存/健康度不串味)。 +/// +/// **取引擎前先把下载设置灌进 core 的下载调优**(M193,按数据根存):`download.concurrency` / +/// `download.retry_limit` / `download.gh_proxy` 变了就重建引擎(值没变时 [`settings::download::apply_tuning`] +/// 一行不动,所以每次调用都灌是安全的)。这是「用户改了设置 ⇒ 下一次下载真的按新值跑」的落点: +/// 不在这里灌,进程级引擎会带着启动时那份参数活到进程结束——那正是「设置页看着有、实际没用」。 #[must_use] pub fn engine(env: &HostEnv) -> Arc { + super::settings::download::apply_tuning(env); shared::engine_for(env.layout.cache()) } @@ -120,7 +126,11 @@ mod tests { let dir = root("cache"); let env = HostEnv::at_root(&dir); let engine = engine(&env); - assert_eq!(engine.config().max_concurrency, 64); + assert_eq!( + engine.config().max_concurrency, + unlucky_core::download::config::DEFAULT_MAX_CONCURRENCY + ); + assert_eq!(engine.concurrency_probe(), engine.config().max_concurrency); // 地区判定不联网:只读缓存/信号 let _ = domestic(&env); std::fs::remove_dir_all(&dir).ok(); diff --git a/crates/presenter/src/host/settings.rs b/crates/presenter/src/host/settings.rs index 572c78e..6c773fb 100644 --- a/crates/presenter/src/host/settings.rs +++ b/crates/presenter/src/host/settings.rs @@ -13,10 +13,14 @@ use crate::dto::{SettingGroupView, SettingRowView}; use super::env::HostEnv; +// M193:下载组的行与「设置 → 引擎调优」的翻译搬进子模块(`settings.rs` 顶在 400 行硬线上, +// 理由与 `settings_tests.rs` 同源,见那个文件的文件头)。 +pub(crate) mod download; + /// 全部设置分组(设置页右栏一次拿全,切分组不再请求)。 pub fn groups(env: &HostEnv) -> Vec { vec![ - download_group(env), + download::group(env), java_group(env), data_group(env), ai_group(env), @@ -58,89 +62,6 @@ fn selected(options: &[&str], value: &str) -> i32 { .unwrap_or(0) } -/// 下载(D7):并发/重试/地区/镜像偏好/代理。 -fn download_group(env: &HostEnv) -> SettingGroupView { - let concurrency = int(env, "download.concurrency", 8).to_string(); - let retry = int(env, "download.retry_limit", 3).to_string(); - let concurrency_options = ["4", "8", "16"]; - let retry_options = ["0", "1", "3", "5"]; - 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( - "download.concurrency", - "并发数", - "同时下载的文件数(海量小文件,16 路足够压满带宽)", - &concurrency_options, - selected(&concurrency_options, &concurrency), - )) - .with(SettingRowView::choice( - "download.retry_limit", - "单文件重试次数", - "失败后换源重试的上限", - &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"), - )) - .with(SettingRowView::action( - super::engine::DETECT_REGION_ACTION, - "重新检测地区", - "清掉 24h 地区缓存,下次下载重新判定", - "detect_region", - )) -} - /// Java(D13/D23):自动安装/内存/选路/发行版优先级。 fn java_group(env: &HostEnv) -> SettingGroupView { let memory = int(env, "java.default_memory_mb", 2048).to_string(); diff --git a/crates/presenter/src/host/settings/download.rs b/crates/presenter/src/host/settings/download.rs new file mode 100644 index 0000000..22c0320 --- /dev/null +++ b/crates/presenter/src/host/settings/download.rs @@ -0,0 +1,174 @@ +//! 设置页「下载」分组 + 下载调优的**宿主侧翻译**(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, +}; +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"]; +/// 单源尝试次数的推荐档位。 +const RETRY_LADDER: [&str; 5] = ["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, + "并发数", + "同时下载的文件数:出厂 8,可调 1~64(引擎硬上限 64,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效", + &concurrency_options, + selected(&concurrency_options, &concurrency), + )) + .with(SettingRowView::choice( + unlucky_core::download::config::KEY_RETRY_LIMIT, + "单源尝试次数", + "同一个源最多试几次(含首次;出厂 3)。换源不受此限:候选链上有几个源就各给几次", + &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)) +} diff --git a/crates/presenter/src/host/settings_tests.rs b/crates/presenter/src/host/settings_tests.rs index 0cabb39..480747a 100644 --- a/crates/presenter/src/host/settings_tests.rs +++ b/crates/presenter/src/host/settings_tests.rs @@ -1,4 +1,8 @@ //! host/settings.rs 的单测(拆文件守 400 行上限)。 +//! +//! **为什么单独一个文件**:`settings.rs` 正好顶在 400 行硬线上,而「设置写进去到底有没有生效」 +//! 这一类用例恰恰需要反复构造数据根 + 读写 `launcher.json`,塞回生产文件就只剩砍论证注释这一条路。 +//! 本文件与它守的代码同目录、同名族(`settings.rs` 的 `#[path]` 挂载),**不是**随手放的杂项目。 use super::*; @@ -7,6 +11,51 @@ fn env_in(dir: &std::path::Path) -> HostEnv { HostEnv::at_root(dir) } +/// **M193 守门**:把设置改成 2 ⇒ **下一次取到的引擎**的并发闸门真是 2(探针读 `Semaphore`)。 +/// +/// 这是「设置项说假话」那条缺陷族的正面守门。断言分三段,缺一段都守不住: +/// +/// 1. 写设置(走 `SetSetting` 同一条 `settings::set`); +/// 2. 走宿主真正的取引擎入口 `host::engine::engine(env)`(它负责把设置灌进 core 调优); +/// 3. 读**引擎的真实闸门** `concurrency_probe()`,不是 `config().max_concurrency`。 +/// +/// 变异证明(只改实现侧): +/// * 把 `host/engine.rs::engine` 里那行 `apply_tuning` 删掉 ⇒ 引擎按出厂 8 建,探针读到 8,红; +/// * 把 `core::download::shared::build` 里的 `tune.apply_to(EngineConfig::default())` 换回 +/// `EngineConfig::default()` ⇒ 同样读到 8,红; +/// * 把 `settings/download.rs::tuning_of` 读错键名(例如 `download.threads`)⇒ 读到默认 8,红。 +/// +/// `download.gh_proxy` 那一路**没有**端到端用例:那个键由 M197 登记,尚未合入 dev,而 +/// `setting::document` 只认已登记的键(未登记的键进不了 `effective()`),所以这里写进文件也读不出来。 +/// core 侧有「调优三档 → 候选链」的守卫(`download/shared.rs`),端到端那一半登记为 M197 的 follow-up。 +#[test] +fn the_concurrency_setting_reaches_the_real_download_engine() { + let dir = std::env::temp_dir().join(format!("ull-host-tune-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let env = env_in(&dir); + + set(&env, "download.concurrency", "2").expect("写并发"); + set(&env, "download.retry_limit", "1").expect("写尝试次数"); + + let engine = crate::host::engine::engine(&env); + assert_eq!(engine.concurrency_probe(), 2, "闸门必须真是 2 张许可"); + assert_eq!( + engine.config().max_attempts_per_source, + 1, + "尝试次数也要吃到设置" + ); + + // 改回去 ⇒ 下一次取到的引擎跟着换(不是「重启才生效」)。 + set(&env, "download.concurrency", "16").expect("再写并发"); + let rebuilt = crate::host::engine::engine(&env); + assert_eq!(rebuilt.concurrency_probe(), 16); + assert!( + !std::sync::Arc::ptr_eq(&engine, &rebuilt), + "参数变了必须换引擎,否则新值落不到在飞的那份上" + ); + std::fs::remove_dir_all(&dir).ok(); +} + #[test] fn groups_cover_the_six_required_areas() { let dir = std::env::temp_dir().join(format!("ull-host-settings-{}", std::process::id())); diff --git a/docs/download-settings.md b/docs/download-settings.md new file mode 100644 index 0000000..c52972b --- /dev/null +++ b/docs/download-settings.md @@ -0,0 +1,119 @@ +# 下载设置到底改了谁(M193) + +一句话:**`download.concurrency` / `download.retry_limit` / `download.gh_proxy` 现在真的进引擎**; +其余四个 `download.*` 键(`region` / `mirror_preference` / `proxy_mode` / `proxy_url`)**引擎侧零消费者**, +界面上改了不会改变任何下载行为——这一页把它们逐条标出来,别再让「设置项说假话」。 + +出处:M190 的 204 条 finding 复核(`DevDocs/PlanDoc/research/191-finding-triage.md` §3 A-3)挖出的 +「用户能改、改了没用」缺口:schema 说并发 8,引擎恒跑 64,两处各写一个数。 + +## 1. 取证:八个下载类键的真实生效值 + +「真实生效值」是**改造前**(M193 之前)代码里实际会用的值,不是界面上写的值。 + +| 键 | schema 默认 / 范围 | 引擎侧消费者 | 真实生效值(改造前) | 本轮 | +|---|---|---|---|---| +| `download.concurrency` | 8 / 1–256 | **无**(恒用 `EngineConfig::default()`) | **64**(`Semaphore` 容量) | ✅ 接进引擎,范围收成 1–64 | +| `download.retry_limit` | 3 / 0–10 | **无** | 3(碰巧与 schema 相同,但改它无效) | ✅ 接进引擎,范围 (1,10) | +| `download.gh_proxy` | (M197 登记,dev 上尚无此键) | JDK 作业那条(M197 自建引擎时覆盖档案) | 地区档案里的内置值(镜像优先时 `gh-proxy.com`) | ✅ 下沉到共享装配点,所有下载路径一致 | +| `download.region` | auto / auto·cn·global | **无**(`RegionProfile::pinned` 无生产调用点) | 永远按「时区+语言」自动判定 | ❌ 未接(见 §5) | +| `download.mirror_preference` | auto / auto·official·mirror | **无** | 永远按地区模式(镜像优先/直连) | ❌ 未接(见 §5) | +| `download.proxy_mode` | none / none·system·manual | **无**(`http::build_client` 从不调 `.proxy()`) | 只走环境变量代理(reqwest 默认),`system`/`manual` 都不生效 | ❌ 未接(见 §5) | +| `download.proxy_url` | "" | **无** | 空(从不使用) | ❌ 未接(见 §5) | +| `download.action.detect_region` | (动作键,不是设置) | presenter 的 `detect_region` | 清 `region.json` + 失效共享引擎 | ✅ 本来就真 | + +复现方法(改造前):`grep -rn "download.concurrency" crates/` 只命中 `setting/keys.rs` 的定义与 +agent 的测试;同理 `download.proxy_mode` / `proxy_url` / `mirror_preference` 只有 presenter 的显示行。 + +## 2. 唯一来源:默认值谁说了算 + +```text +crates/core/src/download/config.rs + DEFAULT_MAX_CONCURRENCY = 8 ← 唯一写死数字的地方 + MAX_CONCURRENCY = 64 ← 硬上限 + DEFAULT_MAX_ATTEMPTS_PER_SOURCE = 3 + MAX_ATTEMPTS_PER_SOURCE = 10 + │ + ├─→ EngineConfig::default() (引擎默认) + ├─→ DownloadTuning::default() (宿主没灌任何值时) + └─→ setting::keys 的 schema 默认值与范围 (界面/AI/CLI 看到的契约) +``` + +守卫 `the_schema_defaults_are_the_engine_defaults`(`download/config.rs`)与 +`the_hard_caps_are_single_sourced` 让「改一处另一个不变」直接变红——这是防 8 vs 64 复发的唯一手段。 + +## 3. 接线链(宿主 → 引擎) + +```text +config/launcher.json + download.concurrency ─┐ + download.retry_limit ─┼→ presenter::host::settings::download::tuning_of + download.gh_proxy ────┘ │ DownloadTuning::from_settings(默认值/夹取/回落都在 core) + ↓ + shared::set_tuning(, tuning) 变了吗?没变就什么都不做 + ↓ 变了 → 淘汰该根的登记项 + shared::engine_for() → shared::build + ↓ + EngineConfig { max_concurrency, max_attempts_per_source } + RegionProfile.gh_proxy + ↓ + Semaphore(容量) / attempts_for() / MirrorTable::expand() 的 gh-proxy 候选 +``` + +**同步点是显式的**,两处,共用同一个函数 `apply_tuning`: + +* `presenter/src/host/engine.rs::engine()` —— 每次取引擎前(幂等,值没变就一行不动); +* `presenter/src/host/dispatch.rs` 的 `SetSetting` 分支 —— 用户改完设置立刻生效,并让常驻持有 + 引擎的自更新 `Factory` 换一份(`refresh_update_factory`)。 + +调优按**数据根**存(与引擎登记处同键),不是进程级:同一进程里多个数据根时互不串味。 + +## 4. 三态语义:`download.gh_proxy` + +| 值 | 含义 | 效果 | +|---|---|---| +| 空串(出厂) | **没有覆盖**,跟随内置默认 | 镜像优先/国内判定时套 `mirrors.json` 里的 `gh-proxy.com` | +| `off`(也认 `none`/`direct`/`disable`/`disabled`/`no`) | 明确关掉 | 一个 gh-proxy 候选都不生成 | +| 其它非空 | 覆盖前缀 | 用它替换内置前缀(类别仍只在镜像优先/国内判定时启用) | + +**空串 ≠ 关**:「我还没决定」和「我明确不要」是两件事。设置页的说明文案(presenter 行 + iced 行) +两档必须都写出来——这是最容易让用户以为「关掉了、其实还在套前缀」的地方。 + +边界:非法值(0、负数、超上限、手改坏值)**一律回落出厂默认**,不报错、不阻断下载; +schema 在写入口就拒绝越界值,引擎侧再夹一次是纵深防御(老文件里可能躺着旧 schema 允许的大数)。 + +## 5. 没接的四个键(已登记为 finding) + +`download.region` / `download.mirror_preference` / `download.proxy_mode` / `download.proxy_url` +**引擎侧零消费者**:界面上改了不改变任何下载行为。要接它们分别需要: + +* `region` → `RegionProfile::pinned(mode, now)`(值 `cn`/`global` 映射到 `MirrorFirst`/`Direct`, + 并把 `trigger` 置成 `UserPinned` 让 24h 重判不覆盖它); +* `mirror_preference` → 引擎候选排序里插一档「强制官方优先 / 强制镜像优先」; +* `proxy_mode` / `proxy_url` → `http::build_client` 里加 `.proxy(reqwest::Proxy::all(url)?)`。 + +三条都在 M193 的 scope 外(`http.rs` / 引擎候选排序 / 一处新装配),已单独立 finding。 + +## 6. 守卫与变异证明(逐条只改实现侧,跑完即还原) + +| # | 变异(只改实现侧) | 守卫用例 | 结果 | +|---|---|---|---| +| M1 | `shared::build` 的 `tune.apply_to(...)` 换回裸 `EngineConfig::default()` | `download::shared::tests::the_setting_and_the_gh_proxy…` | 红(探针 8,期望 2) | +| M2 | `shared::build` 的 `tune.apply_gh_proxy(region)` 换回 `region` | 同上 | 红(`off` 档仍有 gh-proxy 候选) | +| M3 | 删掉 `set_tuning` 里的 `registry().remove` | 同上 | 红(换参数后交回旧引擎) | +| M4 | 删掉构造点的 `clamp(1, MAX_CONCURRENCY)` | 同上 | 红(9999 张许可真开了) | +| M5 | `EngineConfig::default()` 的并发写回字面量 64 | `download::config::tests::the_schema_defaults…` | 红 | +| M6 | schema 的并发默认值写成字面量 16 | 同上 | 红(两处各写一个数) | +| M7 | 删掉 `host/engine.rs::engine` 里的 `apply_tuning` | `host::settings::tests::the_concurrency_setting…` | 红(探针 8,期望 2) | +| M8 | 下载设置页的 gh-proxy 行改回 `("Preferred source", "Automatic (region)")` | `the_downloads_rows_state_semantics…` | 红 | + +## 7. 没验的部分 + +* **真实网络下的并发/重试行为未验**:守卫读的是 `Semaphore` 的真实许可数与 `attempts_for()` 的 + 配置读点;「2 条真连接同时下载时服务器只看到 2 条」和「某个源真的被试 3 次」没有在真网络下跑过 + (离线回环能验连接复用,验不了限流的端到端时序)。 +* **macOS / Linux 未验**:本轮只在 Windows 上编译与跑测试。 +* **`download.gh_proxy` 的端到端未验**:那个键由 M197 登记,尚未合入 dev;`setting::document` 只认 + 已登记的键,所以「写文件 ⇒ 读出来 ⇒ 进候选链」这一段在 dev 上跑不通(core 侧「调优 → 候选链」 + 已守)。M197 合入后应补一条宿主侧端到端用例。 +* **iced 设置页仍然不是可改控件**:它画的是语义行(默认值/区间/三态),真值不显示——因为 iced 侧 + 还没有「presenter 设置快照 → app → `Groups.download`」的数据通路(另立 mission,见 finding)。 diff --git a/docs/images/downloads-settings-zh.png b/docs/images/downloads-settings-zh.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ecf73c5535ed7e47fa3be2d2b3727e37f0c95b GIT binary patch literal 95845 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])LWLYN=_<_^|oB!?B-9u|}%6%5i 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 SOU}VR3*TP+*3maV+VJ1FFQF3v literal 0 HcmV?d00001 From 94f56f9d9cdc67fd0b0b89a93b8add209c43eae2 Mon Sep 17 00:00:00 2001 From: worker-dlmsettings Date: Mon, 9 Nov 2026 02:39:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(m193):=20=E5=A4=8D=E5=AE=A1=20r1=20?= =?UTF-8?q?=E4=B8=89=E6=9D=A1=20P2=E2=80=94=E2=80=94=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B2=E8=BF=98=E5=8E=9F=E3=80=81retry=5Flimit=200=20?= =?UTF-8?q?=E6=9C=89=E8=AF=9A=E5=AE=9E=E8=AF=AD=E4=B9=89=E3=80=81=E8=A3=85?= =?UTF-8?q?=E9=85=8D=E7=82=B9=E6=8E=AA=E8=BE=9E=E6=9B=B4=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2-2:keys.rs 的 schema 版本史按原文逐字还原(1.4.0/1.5.0 的键名、1.2.0 的迁移口径、 旧读新的完整结论),不再用版本文档换行数;腾行改从我自己新加的注释里出。 - P2-1:download.retry_limit 范围恢复 0~10,并把 0 的语义定成「只试一次」(attempts = max(1, 0)): 老配置合法、零迁移、ull config get 读到的 0 与引擎真做的事一致(不再回落出厂 3)。 schema 描述与 presenter/iced 两处 UI 都写清「0 与 1 都 = 只试一次,不代表不下载」。 - P2-3:shared.rs 的「唯一一处装配」更正为「presenter 侧唯一一处」(CLI 还有三处自建), docs 加「哪些路径吃这套设置」表,并立 finding(CLI install/java/pack 不读设置、并发 64→8)。 - P3-3:mirrors.rs 的 gh_proxies 注释改成「无生产读者,仅解析留档」。 - P3-4(presenter 侧):并发/重试描述的出厂值与上限改用常量 format!,不再是第二处字面量。 - 出图重拍(含 0/1 = 只试一次)。 --- .../app-iced/src/pages/settings/download.rs | 2 +- .../pages/settings/tests/download_section.rs | 4 +- crates/core/src/download/config.rs | 145 +++++++++--------- crates/core/src/download/mirrors.rs | 5 +- crates/core/src/download/shared.rs | 51 +++--- crates/core/src/setting/keys.rs | 32 ++-- .../presenter/src/host/settings/download.rs | 14 +- crates/presenter/src/host/settings_tests.rs | 14 ++ docs/download-settings.md | 36 ++++- docs/images/downloads-settings-zh.png | Bin 95845 -> 95838 bytes 10 files changed, 173 insertions(+), 130 deletions(-) diff --git a/crates/app-iced/src/pages/settings/download.rs b/crates/app-iced/src/pages/settings/download.rs index e064dae..5dd288b 100644 --- a/crates/app-iced/src/pages/settings/download.rs +++ b/crates/app-iced/src/pages/settings/download.rs @@ -31,7 +31,7 @@ pub(super) const GROUP: Group = Group { icon: IconKind::Download, rows: &[ ("并发数", "出厂 8 · 可调 1~64"), - ("单源尝试次数", "出厂 3 · 含首次 · 换源另算"), + ("单源尝试次数", "出厂 3 · 含首次 · 0/1 = 只试一次"), ("GitHub 加速前缀", "留空 = 跟随内置默认"), ("关掉它", "填 off = 关闭"), ("换掉它", "填 URL 前缀 = 覆盖"), diff --git a/crates/app-iced/src/pages/settings/tests/download_section.rs b/crates/app-iced/src/pages/settings/tests/download_section.rs index 46b3fc7..1ddd0cb 100644 --- a/crates/app-iced/src/pages/settings/tests/download_section.rs +++ b/crates/app-iced/src/pages/settings/tests/download_section.rs @@ -21,7 +21,7 @@ fn the_downloads_group_shows_concurrency_retries_and_the_gh_proxy_key() { rows, vec![ ("并发数", "出厂 8 · 可调 1~64"), - ("单源尝试次数", "出厂 3 · 含首次 · 换源另算"), + ("单源尝试次数", "出厂 3 · 含首次 · 0/1 = 只试一次"), ("GitHub 加速前缀", "留空 = 跟随内置默认"), ("关掉它", "填 off = 关闭"), ("换掉它", "填 URL 前缀 = 覆盖"), @@ -72,6 +72,8 @@ fn the_downloads_rows_state_semantics_and_never_fake_a_current_value() { "填 URL 前缀 = 覆盖", // 硬上限必须在界面上说清(任务书的边界条款)。 "1~64", + // P2-1:0 是旧版提供过的档位,界面必须说清它 = 只试一次(不是「不下载」)。 + "0/1 = 只试一次", // 三个键各自是哪一行(标签本身也是契约:用户按它认设置)。 "并发数", "单源尝试次数", diff --git a/crates/core/src/download/config.rs b/crates/core/src/download/config.rs index e384ca6..ec5d084 100644 --- a/crates/core/src/download/config.rs +++ b/crates/core/src/download/config.rs @@ -141,8 +141,8 @@ impl DownloadTuning { /// 设置原文 → 调优(**未填 / 越界 / 非法值一律回落出厂默认**,绝不报错阻断下载)。 /// /// 口径与 schema 的 `(min, max)` 同源但独立实现(用户可能手改文件绕过 `store.set`):键不存在 - /// → 出厂默认;落在区间内 → 原值;0 / 负数 / 超上限 → 出厂默认(**不夹到边界**:那是「想 9999 - /// 给 64」,界面仍会说谎)。 + /// → 出厂默认;落在区间内 → 原值;负数 / 超上限 → 出厂默认(**不夹到边界**:那是「想 9999 给 + /// 64」,界面仍会说谎)。`retry_limit` 的 `0` 在区间内,落成「只试一次」(见实现里的注释)。 #[must_use] pub fn from_settings( concurrency: Option, @@ -155,10 +155,12 @@ impl DownloadTuning { .and_then(|raw| usize::try_from(raw).ok()) .filter(|raw| (1..=MAX_CONCURRENCY).contains(raw)) .unwrap_or(default.max_concurrency), + // `0` 是**合法值**(P2-1 复审:旧配置里真的有 0,且旧 GUI 把它当档位):语义是 + // 「只试一次 = 不重试」,所以 0 与 1 都落成 1;负数 / 超上限才回落出厂默认。 max_attempts_per_source: attempts .and_then(|raw| u32::try_from(raw).ok()) - .filter(|raw| (1..=MAX_ATTEMPTS_PER_SOURCE).contains(raw)) - .unwrap_or(default.max_attempts_per_source), + .filter(|raw| *raw <= MAX_ATTEMPTS_PER_SOURCE) + .map_or(default.max_attempts_per_source, |raw| raw.max(1)), gh_proxy: gh_proxy.map_or(GhProxyOverride::Builtin, gh_proxy_override), } } @@ -225,36 +227,47 @@ mod tests { use super::*; use crate::setting::keys::{launcher_schema, launcher_version}; - /// **守门③**:schema 的默认值与 `EngineConfig::default()` / 调优默认值是同一个常量。 + /// **守门③**:schema 的默认值 / 区间 / 键名与 `EngineConfig::default()` / 调优默认值取同一组常量。 /// - /// 变异证明(只改实现侧):把 `EngineConfig::default()` 的 `max_concurrency` 写回字面量 64 - /// ⇒ 本用例红在 `max_concurrency`;把 `setting/keys.rs` 的 `download.concurrency` 默认值写回 - /// 字面量 8 而常量改成 16 ⇒ 红在 schema 一侧。这条防的正是「两处各写一个数」长回来 - /// (8 vs 64 就是这么长出来的)。 + /// 变异证明(只改实现侧):`EngineConfig::default()` 的并发写回字面量 64 ⇒ 红;schema 默认值 + /// 写成字面量 16 ⇒ 红;`MAX_CONCURRENCY` 改了而 schema 仍写 64 ⇒ 红。这条防的正是「两处各写 + /// 一个数」长回来(8 vs 64 就是这么长出来的)。 #[test] - fn the_schema_defaults_are_the_engine_defaults() { + fn the_schema_contract_is_single_sourced() { let schema = launcher_schema(); assert_eq!(schema.version(), launcher_version()); let cfg = EngineConfig::default(); let tune = DownloadTuning::default(); - // 键名常量与 schema 登记是同一份(宿主按常量读,schema 按常量登记)。 - assert!(schema.spec(KEY_CONCURRENCY).is_some(), "{KEY_CONCURRENCY}"); - assert!(schema.spec(KEY_RETRY_LIMIT).is_some(), "{KEY_RETRY_LIMIT}"); - - let concurrency = schema.spec(KEY_CONCURRENCY).expect("键已登记"); - assert_eq!( - concurrency.default, - serde_json::json!(DEFAULT_MAX_CONCURRENCY) - ); + // 并发 1~64(0 张许可没意义);尝试次数 **0**~10(P2-1:0 = 只试一次,旧配置里真有)。 + let contract: [(&str, i64, i64, usize); 2] = [ + ( + KEY_CONCURRENCY, + i64::try_from(DEFAULT_MAX_CONCURRENCY).expect("装得下"), + 1, + MAX_CONCURRENCY, + ), + ( + KEY_RETRY_LIMIT, + i64::from(DEFAULT_MAX_ATTEMPTS_PER_SOURCE), + 0, + usize::try_from(MAX_ATTEMPTS_PER_SOURCE).expect("装得下"), + ), + ]; + for (key, default, min, max) in contract { + let spec = schema.spec(key).expect("键已登记"); + assert_eq!(spec.default, serde_json::json!(default), "{key}"); + assert_eq!( + spec.kind, + crate::setting::spec::ValueKind::Int { + min, + max: i64::try_from(max).expect("装得下"), + }, + "{key}" + ); + } assert_eq!(cfg.max_concurrency, DEFAULT_MAX_CONCURRENCY); assert_eq!(tune.max_concurrency, DEFAULT_MAX_CONCURRENCY); - - let retry = schema.spec(KEY_RETRY_LIMIT).expect("键已登记"); - assert_eq!( - retry.default, - serde_json::json!(DEFAULT_MAX_ATTEMPTS_PER_SOURCE) - ); assert_eq!(cfg.max_attempts_per_source, DEFAULT_MAX_ATTEMPTS_PER_SOURCE); assert_eq!( tune.max_attempts_per_source, @@ -262,44 +275,38 @@ mod tests { ); } - /// 硬上限:schema 的上界与引擎的夹取上限是同一个常量(防「schema 收 256、引擎不知情」)。 - /// 变异证明:只改 `MAX_CONCURRENCY` 而 schema 仍写 64 ⇒ 红。 - #[test] - fn the_hard_caps_are_single_sourced() { - let schema = launcher_schema(); - let concurrency = i64::try_from(MAX_CONCURRENCY).expect("装得下"); - let attempts = i64::from(MAX_ATTEMPTS_PER_SOURCE); - for (key, max) in [(KEY_CONCURRENCY, concurrency), (KEY_RETRY_LIMIT, attempts)] { - let spec = schema.spec(key).expect("键已登记"); - assert_eq!( - spec.kind, - crate::setting::spec::ValueKind::Int { min: 1, max } - ); - } - } - - /// 未填 / 非法值回落默认,不阻断(任务书的边界条款)。 + /// 未填 / 越界回落默认(不阻断下载);**`retry_limit: 0` 是合法值**,语义 = 只试一次。 /// - /// 变异证明:把 `filter(|raw| (1..=MAX_CONCURRENCY).contains(raw))` 删掉 ⇒ `9999` 与 `0` - /// 两处断言红;把 `unwrap_or(default.max_concurrency)` 改成 `unwrap_or(1)` ⇒ `None` 断言红。 + /// 变异证明:把并发的区间过滤删掉 ⇒ `0` / `9999` 两条红;把尝试次数的 `.max(1)` 删掉 ⇒ + /// `0 → 1` 那条红(引擎会拿到 0 次尝试);把 `0` 判成越界 ⇒ 同样红。 #[test] fn missing_or_illegal_values_fall_back_to_the_defaults() { - let tune = DownloadTuning::from_settings(None, None, None); - assert_eq!(tune, DownloadTuning::default()); + assert_eq!( + DownloadTuning::from_settings(None, None, None), + DownloadTuning::default() + ); let just_over = i64::try_from(MAX_CONCURRENCY).expect("上限装得下") + 1; for bad in [0_i64, -1, 9_999, just_over] { - let tune = DownloadTuning::from_settings(Some(bad), Some(bad), None); + let tune = DownloadTuning::from_settings(Some(bad), None, None); + assert_eq!(tune.max_concurrency, DEFAULT_MAX_CONCURRENCY, "{bad}"); + } + // 0 与 1 都 = 只试一次:旧配置里的 0 不许回落成出厂 3(那正是「界面 0、引擎 3」)。 + for zero_or_one in [0_i64, 1] { + let tune = DownloadTuning::from_settings(None, Some(zero_or_one), None); + assert_eq!(tune.max_attempts_per_source, 1, "{zero_or_one}"); + } + for bad in [-1_i64, 9_999] { + let tune = DownloadTuning::from_settings(None, Some(bad), None); assert_eq!( - tune, - DownloadTuning::default(), - "{bad} 越界时应回落默认而不是夹到边界" + tune.max_attempts_per_source, + DEFAULT_MAX_ATTEMPTS_PER_SOURCE ); } - let tune = DownloadTuning::from_settings(Some(2), Some(1), None); + let tune = DownloadTuning::from_settings(Some(2), Some(2), None); assert_eq!(tune.max_concurrency, 2); - assert_eq!(tune.max_attempts_per_source, 1); + assert_eq!(tune.max_attempts_per_source, 2); } /// 三态:**空串 ≠ 关**(这条是「设置项说假话」的高发点,M197 的键语义同源)。 @@ -323,21 +330,21 @@ mod tests { ); } - /// `apply_gh_proxy` 的三档真的改到档案上、并且**改到候选链上**(`MirrorTable::expand()` 读的就是 - /// 档案里那个前缀,内容库/版本清单都走它)。这是「改键 ⇒ 候选链变」的 core 级一半。 + /// `apply_gh_proxy` 的三档真的改到档案上、并且**改到候选链上**(`MirrorTable::expand()` 读的 + /// 就是档案里那个前缀)。端到端(schema 键 → 调优 → `plan()`)由 `shared.rs` 与 presenter 守。 /// 变异证明:把 `Off` 分支改成原样返回 `profile` ⇒ `off` 那条红。 #[test] - fn gh_proxy_override_reaches_the_region_profile_and_the_candidate_chain() { + fn gh_proxy_override_reaches_the_profile_and_the_chain() { use crate::download::mirrors::{MirrorTable, SOURCE_GH_PROXY}; use crate::download::types::ResourceKind; let url = "https://github.com/o/r/releases/download/v1/a.jar"; let table = MirrorTable::builtin(); - let chain = |profile: &RegionProfile| -> Vec { + let sources = |profile: &RegionProfile| -> Vec { table .expand(url, ResourceKind::Other, profile) .into_iter() - .map(|candidate| candidate.url) + .map(|candidate| candidate.source) .collect() }; @@ -345,36 +352,22 @@ mod tests { let kept = DownloadTuning::default().apply_gh_proxy(mirror_first.clone()); assert_eq!(kept.gh_proxy, mirror_first.gh_proxy, "Builtin 不该动档案"); assert!( - chain(&kept).iter().any(|url| url.contains("gh-proxy")), - "出厂默认会套前缀" + sources(&kept).contains(&SOURCE_GH_PROXY.to_string()), + "出厂默认套前缀" ); let off = DownloadTuning::from_settings(None, None, Some("off")) .apply_gh_proxy(mirror_first.clone()); - assert!(!off.gh_proxy_enabled(), "off 必须关掉 gh-proxy"); - assert_eq!(off.gh_proxy, None); + assert!(!off.gh_proxy_enabled()); assert!( - !chain(&off).iter().any(|url| url.contains("gh-proxy")), - "关了就不该有候选" - ); - assert!( - table - .expand(url, ResourceKind::Other, &off) - .iter() - .all(|candidate| candidate.source != SOURCE_GH_PROXY), - "连 source 也不该出现" + !sources(&off).contains(&SOURCE_GH_PROXY.to_string()), + "关了就没有候选" ); let prefix = DownloadTuning::from_settings(None, None, Some("https://gh.example/")) .apply_gh_proxy(mirror_first); assert_eq!(prefix.gh_proxy.as_deref(), Some("https://gh.example/")); assert!(prefix.gh_proxy_enabled()); - assert!( - chain(&prefix) - .iter() - .any(|url| url.starts_with("https://gh.example/")), - "覆盖前缀必须进候选链" - ); } /// 每个数据根的调优互不串味(一个根改成 2 不该改另一个根的引擎)。 diff --git a/crates/core/src/download/mirrors.rs b/crates/core/src/download/mirrors.rs index e48e3bc..31fca8d 100644 --- a/crates/core/src/download/mirrors.rs +++ b/crates/core/src/download/mirrors.rs @@ -130,7 +130,8 @@ pub struct MirrorTable { /// Zulu 元数据(v3 新增)。 #[serde(default)] pub zulu: Option, - /// gh-proxy 类前缀。 + /// gh-proxy 类前缀(**目前无生产读者**:生效前缀的唯一真相源是 `RegionProfile.gh_proxy`; + /// 这一列只作为 `mirrors.json` 的解析留档,别再按它做「改这里能生效」的接口)。 #[serde(default)] pub gh_proxies: Vec, } @@ -184,7 +185,7 @@ impl MirrorTable { /// GitHub host 且允许时套 gh-proxy 前缀。 /// /// **生效前缀的唯一真相源是 `region.gh_proxy`**(设置层/地区判定写入,`None` = 用户关闭)。 - /// 表格里的 `gh_proxies` 只是「已知可用前缀」清单(诊断页可读),**改它不会生效**——M193 + /// 表格里的 `gh_proxies` 只是「已知可用前缀」的留档(无生产读者),**改它不会生效**——M193 /// 删掉了原先那个 `MirrorTable::set_gh_proxy`,正是因为它改了这张表而 `expand()` 读的是档案, /// 属于「看起来能改、其实没人读」的入口。 #[must_use] diff --git a/crates/core/src/download/shared.rs b/crates/core/src/download/shared.rs index f3a59b6..3f0257a 100644 --- a/crates/core/src/download/shared.rs +++ b/crates/core/src/download/shared.rs @@ -136,11 +136,10 @@ pub fn constructions(cache_root: impl AsRef) -> usize { .unwrap_or(0) } -/// 真实构造(**唯一**一处 `DownloadEngine::new` 的宿主侧装配)。装配参数从该数据根的 -/// [`wanted`] 取,不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore` 容量、尝试次数 → -/// `attempts_for()` 上限、gh_proxy → 地区档案(`MirrorTable::expand()` 只认档案里那个前缀,所以 -/// 内容库 / 版本清单 / 字体 / 安装**所有**路径一起生效)。值由宿主翻译后经 [`set_tuning`] 灌入—— -/// core 不认识设置键名,也不依赖 `core::setting`。 +/// 真实构造(**presenter 侧**唯一一处 `DownloadEngine::new` 的宿主侧装配;CLI 还有三处自建, +/// 见 `docs/download-settings.md` §3 的「哪些路径吃这套设置」)。装配参数从该数据根的 [`wanted`] 取, +/// 不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore` 容量、尝试次数 → `attempts_for()` +/// 上限、gh_proxy → 地区档案(`expand()` 只认档案里那个前缀)。值由宿主经 [`set_tuning`] 灌入。 fn build(cache_root: &Path) -> DownloadEngine { let now = now_ms(); let tune = wanted(cache_root); @@ -179,7 +178,7 @@ pub fn set_tuning(cache_root: impl AsRef, next: DownloadTuning) -> bool { mod tests { use super::*; - /// 每个用例一个独立数据根(登记处是进程级的,键撞了会互相干扰)。 + /// 每个用例一个独立数据根(登记处与调优都是进程级、按根存,键撞了会互相干扰)。 fn root(tag: &str) -> PathBuf { static SEQ: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -325,17 +324,16 @@ mod tests { std::fs::remove_dir_all(&dir).ok(); } - /// **守门①②**:设置说 2 ⇒ 引擎**真实的并发闸门**就是 2(探针读 `Semaphore`,不是读配置); - /// 同一口装配点上的 gh-proxy 三档也真的改变 `plan()` 的候选链。 + /// **守门①②**:设置说 2 ⇒ 引擎**真实的并发闸门**就是 2(探针读 `Semaphore` 而非配置);gh-proxy + /// 三档也真的改变 `plan()` 的候选链;`retry_limit = 0` 落成「只试一次」。 /// - /// 变异证明(只改实现侧):① `build` 的 `tune.apply_to(...)` 换回裸 `EngineConfig::default()` - /// ⇒ 探针 8,红;② `tune.apply_gh_proxy(region)` 换回 `region` ⇒ `off` 档仍有候选,红; - /// ③ 删掉 `set_tuning` 里那条 `remove` ⇒ 换参数后交回旧引擎(探针 2 ≠ 5),红。 - /// gh-proxy 的**端到端**一半(schema 键 → 调优)要等 M197 登记该键,这里只守「调优 → 候选链」。 + /// 变异证明(只改实现侧):① `build` 换回裸 `EngineConfig::default()` ⇒ 探针 8 红;② + /// `apply_gh_proxy(region)` 换回 `region` ⇒ `off` 档仍有候选,红;③ 删 `set_tuning` 的 + /// `remove` ⇒ 换参数后交回旧引擎,红。gh-proxy 的端到端(schema 键 → 调优)等 M197 登记该键。 #[test] fn the_setting_and_the_gh_proxy_override_reach_the_engine() { use super::super::types::{DownloadRequest, ResourceKind}; - use super::super::RegionProfile; + use super::super::{config, RegionProfile}; let dir = root("tune"); // 镜像优先的档案:gh-proxy 只在这个模式下才生成候选。 @@ -363,13 +361,8 @@ mod tests { set(Some(2), Some(1), "off"); let engine = engine_for(&dir); assert_eq!(engine.concurrency_probe(), 2, "闸门必须真是 2 张许可"); - assert_eq!( - engine.config().max_attempts_per_source, - 1, - "尝试次数同一条路" - ); let off = chain(&dir); - assert!(!off.iter().any(|url| url.contains("gh-proxy")), "{off:?}"); + assert!(!off.iter().any(|u| u.contains("gh-proxy")), "{off:?}"); set(Some(5), None, "https://gh.example/"); let rebuilt = engine_for(&dir); @@ -377,23 +370,27 @@ mod tests { assert_eq!(rebuilt.concurrency_probe(), 5, "新闸门吃新值"); let over = chain(&dir); assert!( - over.iter() - .any(|url| url.starts_with("https://gh.example/")), - "覆盖前缀必须出现在候选链里:{over:?}" + over.iter().any(|u| u.starts_with("https://gh.example/")), + "{over:?}" ); - // 硬上限(纵深防御):宿主绕开 `from_settings` 直接塞 9999,闸门也只能开到 64。 + // P2-1:`retry_limit = 0`(旧配置里真有)不许回落出厂 3。硬上限:塞 9999 也只开 64。 + set(Some(8), Some(0), ""); + let zero = engine_for(&dir); + assert_eq!(zero.config().max_attempts_per_source, 1); + assert_eq!( + zero.attempts_for("official", ResourceKind::Other, now_ms()), + 1 + ); let wild = DownloadTuning { max_concurrency: 9_999, - ..DownloadTuning::default() + ..Default::default() }; set_tuning(&dir, wild); assert_eq!( engine_for(&dir).concurrency_probe(), - super::super::config::MAX_CONCURRENCY, - "9999 张许可不能真开" + config::MAX_CONCURRENCY ); - std::fs::remove_dir_all(&dir).ok(); } } diff --git a/crates/core/src/setting/keys.rs b/crates/core/src/setting/keys.rs index 687e3ba..177d81d 100644 --- a/crates/core/src/setting/keys.rs +++ b/crates/core/src/setting/keys.rs @@ -4,8 +4,6 @@ use super::catalog::SettingSchema; use super::schema::{SchemaId, SchemaVersion}; use super::spec::{AiPolicy, SettingSpec}; - -// M193:下载两键的默认值与硬上限的唯一来源在 `download::config`(引擎侧),这里只登记成 schema。 use crate::download::config as download; /// `core.data_mode` 允许值。 @@ -48,18 +46,23 @@ pub mod update; /// /// `1.1.0` 登记 D20 实例隔离键(`instances.mode` 等)。 /// `1.2.0` 登记 D23 的 JDK 落点与发行版优先级键(`java.installRoot` / `java.vendorPriority`): -/// 只推 minor、不强制迁移——[`super::document`] 的「默认值不落盘」让老文件缺键即取默认。 +/// 同样只推 minor、不强制迁移——[`super::document`] 的「默认值不落盘」让老文件缺键即取默认, +/// 迁移链(按 major 走)不需要新步骤;`1.1.0 → 1.2.0` 的差异只体现在文件里出现新键之后。 /// `1.3.0` 登记 D17 的「外观(高级)」P1 键(`appearance.bg_image*` / `panel_alpha` / `glass` / -/// `accent_color`)与 `1.5.0` 的窗口圆角、`1.4.0` 的外观下层三键(M91):都是纯增量—— -/// 出厂全默认即等于老文件缺键。 -/// `1.6.0` 登记 M161 的「自更新」两键(见 [`update`]):纯增量 + 一步迁移(见 [`super::update_prefs`])。 +/// `accent_color`):同一套纯增量语义——出厂全默认(高级特性全关)即等于老文件缺键。 +/// `1.4.0` 登记 P4 的外观下层三键(`window_opacity` / `titlebar_mode` / `palette_source`,M91): +/// 仍然只推 minor、不加迁移步骤——三者的出厂值正是「老文件没有这些键」时的等价物。 +/// `1.5.0` 登记 M116 的窗口圆角策略(`appearance.window_corner`,出厂 `system`):纯增量。 +/// `1.6.0` 登记 M161 的「自更新」两键(见 [`update`]):纯增量但带一步数据迁移 +/// (此前住在 `cache/update/prefs.json`,迁移见 [`super::update_prefs`])。 /// `1.7.0` 登记 M168 的中文字体下载地址覆盖键(`ui.font_cdn`,见 [`font`]):纯增量、无迁移。 /// /// **旧读新(方向相反,措辞必须精确)**:1.4.0 的客户端读 1.5.0 的文件时, /// [`super::schema::SchemaCompat::check_url`] 走 `found.minor > expected.minor` → -/// `ReadOnlyPreserveSchema` → `FileAccess::ReadOnly`:**整份配置对它只读**(值照读、`store.set` -/// 一律 `SettingError::ReadOnly`),不是「只是少一档」。这正是新键必须推 minor 的原因—— -/// 回退到旧版本会出现「设置改不动」,而原因只在 `$schema` 那一行里。 +/// `ReadOnlyPreserveSchema` → [`super::schema::SchemaCompat::access`] → `FileAccess::ReadOnly`。 +/// 也就是说**整份配置对它只读**(值照读、`store.set` 一律 `SettingError::ReadOnly`、`save` 跳过), +/// 不是「只是少一档」。这是 minor 提升的既定代价——也正是新键必须推 minor 而不是偷偷塞进旧版本的 +/// 原因:回退到旧版本会出现「设置改不动」,而原因只在 `$schema` 那一行里,不看版本号无从排查。 #[must_use] pub fn launcher_version() -> SchemaVersion { SchemaVersion::new(1, 7, 0) @@ -100,8 +103,6 @@ pub fn launcher_schema() -> SettingSchema { AiPolicy::sensitive(), "实例默认隔离模式:isolated(运行目录派生 versions/<版本 id>,D20 默认)/ shared(所有实例共用 .minecraft 根)", )) - // M193:默认值/区间只有一个来源——`download::config` 的常量。此前这里写 8、 - // `EngineConfig::default()` 写 64,界面说「当前 8」引擎却跑 64,调并发完全无效。 .with(SettingSpec::int( "download.concurrency", download::DEFAULT_MAX_CONCURRENCY as i64, @@ -112,9 +113,9 @@ pub fn launcher_schema() -> SettingSchema { .with(SettingSpec::int( "download.retry_limit", download::DEFAULT_MAX_ATTEMPTS_PER_SOURCE as i64, - (1, download::MAX_ATTEMPTS_PER_SOURCE as i64), + (0, download::MAX_ATTEMPTS_PER_SOURCE as i64), AiPolicy::silent(), - "单个下载源的最多尝试次数(含首次,1 = 不重试;出厂 3)。换源不受此限:候选链上有几个源就各给几次", + "单个下载源的最多尝试次数(含首次;出厂 3)。0 与 1 都等于「只试一次、不重试」——0 是旧版 GUI 提供过的档位,保留合法但不代表「不下载」。换源不受此限", )) .with(SettingSpec::enumeration( "download.region", @@ -227,9 +228,8 @@ pub fn launcher_schema() -> SettingSchema { AiPolicy::locked(), "LLM API 密钥——AI 永不读写的凭据(D9)", )) - // M39(P3 MCP server,02 §0「传输定论」):对外 agent 工具服务。 - // 三个开关默认「关 / 只读」——外部客户端拿到的是与内置客户端**同一注册表**的工具, - // 但写权限必须用户显式打开(02 §1「同一注册表、同一安全策略、同一审计日志」)。 + // M39(P3 MCP server,02 §0「传输定论」):对外 agent 工具服务。三个开关默认「关 / 只读」—— + // 外部客户端拿到的是与内置客户端**同一注册表**的工具,但写权限必须用户显式打开(02 §1)。 .with(SettingSpec::bool( "agent.mcp.enabled", false, diff --git a/crates/presenter/src/host/settings/download.rs b/crates/presenter/src/host/settings/download.rs index 22c0320..4e6be69 100644 --- a/crates/presenter/src/host/settings/download.rs +++ b/crates/presenter/src/host/settings/download.rs @@ -22,7 +22,7 @@ //! 「读起来是纯读、实际改全局状态」正是本仓吃过亏的那类形状。 use unlucky_core::download::config::{ - DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY, + DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY, MAX_CONCURRENCY, }; use unlucky_core::download::shared; @@ -33,8 +33,8 @@ use super::{int, selected, text}; /// 并发数的推荐档位(`selected()` 找不到时回 0;手改出来的非档位值会被插进选项表,见 [`group`])。 const CONCURRENCY_LADDER: [&str; 5] = ["4", "8", "16", "32", "64"]; -/// 单源尝试次数的推荐档位。 -const RETRY_LADDER: [&str; 5] = ["1", "2", "3", "5", "10"]; +/// 单源尝试次数的推荐档位(`0` 是旧版 GUI 提供过的档位,语义 = 只试一次,见 schema 描述)。 +const RETRY_LADDER: [&str; 6] = ["0", "1", "2", "3", "5", "10"]; /// 下载(D7):并发/重试/地区/镜像偏好/代理/GitHub 加速前缀。 pub(super) fn group(env: &HostEnv) -> SettingGroupView { @@ -64,14 +64,18 @@ pub(super) fn group(env: &HostEnv) -> SettingGroupView { .with(SettingRowView::choice( unlucky_core::download::config::KEY_CONCURRENCY, "并发数", - "同时下载的文件数:出厂 8,可调 1~64(引擎硬上限 64,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效", + &format!( + "同时下载的文件数:出厂 {DEFAULT_MAX_CONCURRENCY},可调 1~{MAX_CONCURRENCY}(引擎硬上限,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效" + ), &concurrency_options, selected(&concurrency_options, &concurrency), )) .with(SettingRowView::choice( unlucky_core::download::config::KEY_RETRY_LIMIT, "单源尝试次数", - "同一个源最多试几次(含首次;出厂 3)。换源不受此限:候选链上有几个源就各给几次", + &format!( + "同一个源最多试几次(含首次;出厂 {DEFAULT_MAX_ATTEMPTS_PER_SOURCE})。0 与 1 都等于「只试一次、不重试」——0 是旧版提供的档位,不代表「不下载」。换源不受此限" + ), &retry_options, selected(&retry_options, &retry), )) diff --git a/crates/presenter/src/host/settings_tests.rs b/crates/presenter/src/host/settings_tests.rs index 480747a..3dd3b7b 100644 --- a/crates/presenter/src/host/settings_tests.rs +++ b/crates/presenter/src/host/settings_tests.rs @@ -45,6 +45,20 @@ fn the_concurrency_setting_reaches_the_real_download_engine() { "尝试次数也要吃到设置" ); + // P2-1:`retry_limit = 0`(旧配置里真有)必须写得回去、且引擎真的只试一次(不是出厂 3)。 + set(&env, "download.retry_limit", "0").expect("0 是合法值(旧版档位)"); + let zero = crate::host::engine::engine(&env); + println!( + "retry_limit=0 => store={:?} engine.config().max_attempts_per_source={}", + env.store().doc().effective_i64("download.retry_limit"), + zero.config().max_attempts_per_source + ); + assert_eq!( + zero.config().max_attempts_per_source, + 1, + "0 必须落成「只试一次」,不许回落出厂 3" + ); + // 改回去 ⇒ 下一次取到的引擎跟着换(不是「重启才生效」)。 set(&env, "download.concurrency", "16").expect("再写并发"); let rebuilt = crate::host::engine::engine(&env); diff --git a/docs/download-settings.md b/docs/download-settings.md index c52972b..0c0ff43 100644 --- a/docs/download-settings.md +++ b/docs/download-settings.md @@ -67,6 +67,21 @@ config/launcher.json 调优按**数据根**存(与引擎登记处同键),不是进程级:同一进程里多个数据根时互不串味。 +## 3.1 哪些路径吃这套设置(哪些不吃) + +| 路径 | 入口 | 吃设置吗 | +|---|---|---| +| GUI 的**全部**下载:内容库(Modrinth/CurseForge)、版本清单、资源/库/日志文件、字体、JDK 安装、自更新 | `presenter::host::engine::engine` → `core::download::shared::engine_for` | ✅ 吃(并发/尝试次数/gh-proxy 都吃) | +| CLI `ull install` | `crates/cli/src/commands/install/mod.rs:95` 自建 `DownloadEngine::new(EngineConfig::default())` | ❌ **不吃**:并发与尝试次数是**出厂值**(8 / 3),不是用户设置值 | +| CLI `ull java` | `crates/cli/src/commands/java.rs:326` 同上 | ❌ 不吃 | +| CLI `ull pack` | `crates/cli/src/commands/pack.rs:286` 同上 | ❌ 不吃 | + +也就是说 `shared::build` 是 **presenter 侧**唯一一处宿主装配,但**不是全仓唯一**:CLI 有三处自建引擎, +它们在 M193 之前也不读设置(一直是裸 `EngineConfig::default()`),而 M193 把默认并发从 64 改成 8 之后, +`ull install` / `ull java` / `ull pack` 的并发**跟着从 64 变成 8**(出厂值变化,不是读到了设置)。 +要让它们也吃设置,得改成走 `shared::engine_for` + 从 CLI 自己的 store 灌 tuning —— 已立 finding, +不在 M193 的 scope(`crates/cli`)。 + ## 4. 三态语义:`download.gh_proxy` | 值 | 含义 | 效果 | @@ -78,8 +93,13 @@ config/launcher.json **空串 ≠ 关**:「我还没决定」和「我明确不要」是两件事。设置页的说明文案(presenter 行 + iced 行) 两档必须都写出来——这是最容易让用户以为「关掉了、其实还在套前缀」的地方。 -边界:非法值(0、负数、超上限、手改坏值)**一律回落出厂默认**,不报错、不阻断下载; -schema 在写入口就拒绝越界值,引擎侧再夹一次是纵深防御(老文件里可能躺着旧 schema 允许的大数)。 +边界:非法值(负数、超上限、手改坏值)**一律回落出厂默认**,不报错、不阻断下载;schema 在写入口 +就拒绝越界值,引擎侧再夹一次是纵深防御(老文件里可能躺着旧 schema 允许的大数)。 + +**`download.retry_limit = 0` 是合法值**(P2-1 复审的结论):旧版 GUI 的档位表里就有 `0`,所以 +范围保留 `0~10`,语义定为 **`0` 与 `1` 都 = 只试一次、不重试**(`attempts = max(1, value)`)。 +这样老配置零迁移、`ull config get` 读到的 0 与引擎真做的事一致(试 1 次),不会再出现 +「界面显示 0、引擎按 3 跑」。`) ## 5. 没接的四个键(已登记为 finding) @@ -106,6 +126,18 @@ schema 在写入口就拒绝越界值,引擎侧再夹一次是纵深防御( | M7 | 删掉 `host/engine.rs::engine` 里的 `apply_tuning` | `host::settings::tests::the_concurrency_setting…` | 红(探针 8,期望 2) | | M8 | 下载设置页的 gh-proxy 行改回 `("Preferred source", "Automatic (region)")` | `the_downloads_rows_state_semantics…` | 红 | +## 6.1 已知的观感问题(P3-1,归属下一条 mission) + +下载设置页把「GitHub 加速前缀」的三态画成了三行(`GitHub 加速前缀 / 留空 = 跟随内置默认`、 +`关掉它 / 填 off = 关闭`、`换掉它 / 填 URL 前缀 = 覆盖`)。这是**已知的观感问题**: +①`关掉它` / `换掉它` 读起来像按钮而不是取值说明;②同一个设置画成三行比三态本身更难懂。 + +不改的理由不是不重要,而是渲染器不支持「一行 + 副说明」:`pages/settings/frame.rs` 的 +`setting_row` 只画「左标签 + 右单行值」,没有副说明位;要改得动 `frame.rs`(M196 正在改它)。 +正确的家是**下一条「iced 设置数据通路」mission**——那三行会变成真控件,形状问题自然消失。 +(同一条 finding 里还记着 presenter 侧描述里的 `1~64` / 出厂值:presenter 已改成用常量 `format!`, +iced 侧的 `rows` 是 `&'static str`,仍是**已知的第二真相源**,等真控件一并解决。) + ## 7. 没验的部分 * **真实网络下的并发/重试行为未验**:守卫读的是 `Semaphore` 的真实许可数与 `attempts_for()` 的 diff --git a/docs/images/downloads-settings-zh.png b/docs/images/downloads-settings-zh.png index a9ecf73c5535ed7e47fa3be2d2b3727e37f0c95b..e3cef2dc386ec5831bef324a52c5e0590946adfd 100644 GIT binary patch delta 54959 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Μp7>;RkSX$Su4d7QJ@j0ajb0$EC5+7@Az&BcjPs0 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 P|1JGs`TKeAZP@?ctS}~u delta 55103 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 D>Mc__ From 54dde7f494a156288f6e61a7f5329094322733d8 Mon Sep 17 00:00:00 2001 From: worker-dlmsettings Date: Mon, 9 Nov 2026 03:04:18 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(m193):=20=E5=A4=8D=E5=AE=A1=20r2?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8F=B0=E8=B4=A6=E4=B8=89=E5=A4=84=E5=A4=B1?= =?UTF-8?q?=E6=95=88=E5=BC=95=E7=94=A8=E3=80=81M39=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E9=80=90=E5=AD=97=E8=BF=98=E5=8E=9F=E3=80=81schema=20doc=20?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E8=A1=A5=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2-1:docs 的契约表把 retry_limit 范围从 (1,10) 改成 0–10(0 与 1 都 = 只试一次), 与 §4 和 keys.rs 对齐;§2 与 §6 的守卫名统一成 the_schema_contract_is_single_sourced (原两条已合并),并把变异 M9(去掉 .max(1) ⇒ 红)补进 §6 台账。 - P3-4:keys.rs 的 M39(MCP server)既有注释按 origin/dev 原文逐字还原(含「同一安全策略、 同一审计日志」);腾出的 1 行用「去掉 alias use、两处常量改全路径」换,没有压任何既有论证。 比对:版本史 / D20 / M39 三段与 origin/dev 逐字节相同。 - P3-2:SettingSpec.doc 是 &'static str(concat! 插不进常量),描述里的数字是第三处第二来源, 现在有守卫 the_key_docs_quote_the_real_limits(改 1~64 → 1~99 ⇒ 红,变异 M10 已跑)。 §6.1.1 用表登记三处「数字的第二来源」现状。 - P3-1:docs 里那个游离反引号删掉。 --- crates/core/src/download/config.rs | 33 ++++++++++++++++++------------ crates/core/src/setting/keys.rs | 14 ++++++------- docs/download-settings.md | 24 +++++++++++++++++----- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/crates/core/src/download/config.rs b/crates/core/src/download/config.rs index ec5d084..ddcda41 100644 --- a/crates/core/src/download/config.rs +++ b/crates/core/src/download/config.rs @@ -184,8 +184,8 @@ impl DownloadTuning { } } -/// 每个缓存根一份「宿主想要什么」(`engine_for` 构造时用它,见 [`wanted`])。按数据根而不是进程级: -/// 登记处本就是「一个数据根一个引擎」;进程级那份在多数据根时会互相踩(A 根改 2、B 根冲回 8)。 +/// 每个缓存根一份「宿主想要什么」([`wanted`] 用)。按数据根而不是进程级:登记处本就是「一个数据根 +/// 一个引擎」;进程级那份在多数据根时会互相踩(A 根改 2、B 根冲回 8)。 fn wanted_slot() -> &'static Mutex> { static WANTED: OnceLock>> = OnceLock::new(); WANTED.get_or_init(|| Mutex::new(HashMap::new())) @@ -228,10 +228,8 @@ mod tests { use crate::setting::keys::{launcher_schema, launcher_version}; /// **守门③**:schema 的默认值 / 区间 / 键名与 `EngineConfig::default()` / 调优默认值取同一组常量。 - /// - /// 变异证明(只改实现侧):`EngineConfig::default()` 的并发写回字面量 64 ⇒ 红;schema 默认值 - /// 写成字面量 16 ⇒ 红;`MAX_CONCURRENCY` 改了而 schema 仍写 64 ⇒ 红。这条防的正是「两处各写 - /// 一个数」长回来(8 vs 64 就是这么长出来的)。 + /// 变异证明(只改实现侧):`EngineConfig::default()` 的并发写回 64、schema 默认值写成 16、 + /// `MAX_CONCURRENCY` 改了而 schema 仍写 64 ⇒ 各自红。 #[test] fn the_schema_contract_is_single_sourced() { let schema = launcher_schema(); @@ -275,6 +273,17 @@ mod tests { ); } + /// `SettingSpec.doc` 是 `&'static str`(`concat!` 插不进常量),描述里的数字只能手写——这条钉住它, + /// 改常量不改文案就红(`catalog.rs` 会把这些 doc 当 JSON schema 的 description 发出去)。 + #[test] + fn the_key_docs_quote_the_real_limits() { + let schema = launcher_schema(); + let doc = |key: &str| schema.spec(key).expect("键已登记").doc; + assert!(doc(KEY_CONCURRENCY).contains(&format!("1~{MAX_CONCURRENCY}"))); + assert!(doc(KEY_CONCURRENCY).contains(&format!("出厂 {DEFAULT_MAX_CONCURRENCY}"))); + assert!(doc(KEY_RETRY_LIMIT).contains(&format!("出厂 {DEFAULT_MAX_ATTEMPTS_PER_SOURCE}"))); + } + /// 未填 / 越界回落默认(不阻断下载);**`retry_limit: 0` 是合法值**,语义 = 只试一次。 /// /// 变异证明:把并发的区间过滤删掉 ⇒ `0` / `9999` 两条红;把尝试次数的 `.max(1)` 删掉 ⇒ @@ -330,9 +339,8 @@ mod tests { ); } - /// `apply_gh_proxy` 的三档真的改到档案上、并且**改到候选链上**(`MirrorTable::expand()` 读的 - /// 就是档案里那个前缀)。端到端(schema 键 → 调优 → `plan()`)由 `shared.rs` 与 presenter 守。 - /// 变异证明:把 `Off` 分支改成原样返回 `profile` ⇒ `off` 那条红。 + /// `apply_gh_proxy` 的三档真的改到档案上、并且**改到候选链上**(`expand()` 读的就是档案里那个 + /// 前缀;自定义前缀那一档的链断言在 `shared.rs`)。变异证明:`Off` 分支改成原样返回 ⇒ `off` 红。 #[test] fn gh_proxy_override_reaches_the_profile_and_the_chain() { use crate::download::mirrors::{MirrorTable, SOURCE_GH_PROXY}; @@ -353,21 +361,20 @@ mod tests { assert_eq!(kept.gh_proxy, mirror_first.gh_proxy, "Builtin 不该动档案"); assert!( sources(&kept).contains(&SOURCE_GH_PROXY.to_string()), - "出厂默认套前缀" + "出厂套前缀" ); let off = DownloadTuning::from_settings(None, None, Some("off")) .apply_gh_proxy(mirror_first.clone()); - assert!(!off.gh_proxy_enabled()); assert!( !sources(&off).contains(&SOURCE_GH_PROXY.to_string()), - "关了就没有候选" + "关了就没候选" ); let prefix = DownloadTuning::from_settings(None, None, Some("https://gh.example/")) .apply_gh_proxy(mirror_first); assert_eq!(prefix.gh_proxy.as_deref(), Some("https://gh.example/")); - assert!(prefix.gh_proxy_enabled()); + assert!(prefix.gh_proxy_enabled(), "前缀生效"); } /// 每个数据根的调优互不串味(一个根改成 2 不该改另一个根的引擎)。 diff --git a/crates/core/src/setting/keys.rs b/crates/core/src/setting/keys.rs index 177d81d..be443ec 100644 --- a/crates/core/src/setting/keys.rs +++ b/crates/core/src/setting/keys.rs @@ -4,7 +4,6 @@ use super::catalog::SettingSchema; use super::schema::{SchemaId, SchemaVersion}; use super::spec::{AiPolicy, SettingSpec}; -use crate::download::config as download; /// `core.data_mode` 允许值。 pub const DATA_MODES: &[&str] = &["portable", "standard", "custom"]; @@ -105,15 +104,15 @@ pub fn launcher_schema() -> SettingSchema { )) .with(SettingSpec::int( "download.concurrency", - download::DEFAULT_MAX_CONCURRENCY as i64, - (1, download::MAX_CONCURRENCY as i64), + crate::download::config::DEFAULT_MAX_CONCURRENCY as i64, + (1, crate::download::config::MAX_CONCURRENCY as i64), AiPolicy::silent(), "同时下载的文件数(1~64,出厂 8)。这是引擎真实持有的并发许可,不是展示值;越界或手改坏值按出厂值生效", )) .with(SettingSpec::int( "download.retry_limit", - download::DEFAULT_MAX_ATTEMPTS_PER_SOURCE as i64, - (0, download::MAX_ATTEMPTS_PER_SOURCE as i64), + crate::download::config::DEFAULT_MAX_ATTEMPTS_PER_SOURCE as i64, + (0, crate::download::config::MAX_ATTEMPTS_PER_SOURCE as i64), AiPolicy::silent(), "单个下载源的最多尝试次数(含首次;出厂 3)。0 与 1 都等于「只试一次、不重试」——0 是旧版 GUI 提供过的档位,保留合法但不代表「不下载」。换源不受此限", )) @@ -228,8 +227,9 @@ pub fn launcher_schema() -> SettingSchema { AiPolicy::locked(), "LLM API 密钥——AI 永不读写的凭据(D9)", )) - // M39(P3 MCP server,02 §0「传输定论」):对外 agent 工具服务。三个开关默认「关 / 只读」—— - // 外部客户端拿到的是与内置客户端**同一注册表**的工具,但写权限必须用户显式打开(02 §1)。 + // M39(P3 MCP server,02 §0「传输定论」):对外 agent 工具服务。 + // 三个开关默认「关 / 只读」——外部客户端拿到的是与内置客户端**同一注册表**的工具, + // 但写权限必须用户显式打开(02 §1「同一注册表、同一安全策略、同一审计日志」)。 .with(SettingSpec::bool( "agent.mcp.enabled", false, diff --git a/docs/download-settings.md b/docs/download-settings.md index 0c0ff43..ded21a9 100644 --- a/docs/download-settings.md +++ b/docs/download-settings.md @@ -14,7 +14,7 @@ | 键 | schema 默认 / 范围 | 引擎侧消费者 | 真实生效值(改造前) | 本轮 | |---|---|---|---|---| | `download.concurrency` | 8 / 1–256 | **无**(恒用 `EngineConfig::default()`) | **64**(`Semaphore` 容量) | ✅ 接进引擎,范围收成 1–64 | -| `download.retry_limit` | 3 / 0–10 | **无** | 3(碰巧与 schema 相同,但改它无效) | ✅ 接进引擎,范围 (1,10) | +| `download.retry_limit` | 3 / 0–10 | **无** | 3(碰巧与 schema 相同,但改它无效) | ✅ 接进引擎,范围 0–10(0 与 1 都 = 只试一次) | | `download.gh_proxy` | (M197 登记,dev 上尚无此键) | JDK 作业那条(M197 自建引擎时覆盖档案) | 地区档案里的内置值(镜像优先时 `gh-proxy.com`) | ✅ 下沉到共享装配点,所有下载路径一致 | | `download.region` | auto / auto·cn·global | **无**(`RegionProfile::pinned` 无生产调用点) | 永远按「时区+语言」自动判定 | ❌ 未接(见 §5) | | `download.mirror_preference` | auto / auto·official·mirror | **无** | 永远按地区模式(镜像优先/直连) | ❌ 未接(见 §5) | @@ -39,8 +39,9 @@ crates/core/src/download/config.rs └─→ setting::keys 的 schema 默认值与范围 (界面/AI/CLI 看到的契约) ``` -守卫 `the_schema_defaults_are_the_engine_defaults`(`download/config.rs`)与 -`the_hard_caps_are_single_sourced` 让「改一处另一个不变」直接变红——这是防 8 vs 64 复发的唯一手段。 +守卫 `the_schema_contract_is_single_sourced`(`download/config.rs`,它合并了原先的「默认值相等」与 +「区间同源」两条)让「改一处另一个不变」直接变红——这是防 8 vs 64 复发的唯一手段。 +另有 `the_key_docs_quote_the_real_limits` 钉住 schema 描述里的数字(见 §6.1 的第三处字面量)。 ## 3. 接线链(宿主 → 引擎) @@ -99,7 +100,7 @@ config/launcher.json **`download.retry_limit = 0` 是合法值**(P2-1 复审的结论):旧版 GUI 的档位表里就有 `0`,所以 范围保留 `0~10`,语义定为 **`0` 与 `1` 都 = 只试一次、不重试**(`attempts = max(1, value)`)。 这样老配置零迁移、`ull config get` 读到的 0 与引擎真做的事一致(试 1 次),不会再出现 -「界面显示 0、引擎按 3 跑」。`) +「界面显示 0、引擎按 3 跑」。 ## 5. 没接的四个键(已登记为 finding) @@ -121,10 +122,12 @@ config/launcher.json | M2 | `shared::build` 的 `tune.apply_gh_proxy(region)` 换回 `region` | 同上 | 红(`off` 档仍有 gh-proxy 候选) | | M3 | 删掉 `set_tuning` 里的 `registry().remove` | 同上 | 红(换参数后交回旧引擎) | | M4 | 删掉构造点的 `clamp(1, MAX_CONCURRENCY)` | 同上 | 红(9999 张许可真开了) | -| M5 | `EngineConfig::default()` 的并发写回字面量 64 | `download::config::tests::the_schema_defaults…` | 红 | +| M5 | `EngineConfig::default()` 的并发写回字面量 64 | `download::config::tests::the_schema_contract_is_single_sourced` | 红 | | M6 | schema 的并发默认值写成字面量 16 | 同上 | 红(两处各写一个数) | | M7 | 删掉 `host/engine.rs::engine` 里的 `apply_tuning` | `host::settings::tests::the_concurrency_setting…` | 红(探针 8,期望 2) | | M8 | 下载设置页的 gh-proxy 行改回 `("Preferred source", "Automatic (region)")` | `the_downloads_rows_state_semantics…` | 红 | +| M9 | `from_settings` 的尝试次数去掉 `.max(1)`(0 原样交给引擎) | `download::config::tests::missing_or_illegal_values_fall_back_to_the_defaults` | 红(`left: 0 / right: 1`) | +| M10 | schema 描述里的「1~64」改成「1~99」(第三处字面量) | `download::config::tests::the_key_docs_quote_the_real_limits` | 红 | ## 6.1 已知的观感问题(P3-1,归属下一条 mission) @@ -138,6 +141,17 @@ config/launcher.json (同一条 finding 里还记着 presenter 侧描述里的 `1~64` / 出厂值:presenter 已改成用常量 `format!`, iced 侧的 `rows` 是 `&'static str`,仍是**已知的第二真相源**,等真控件一并解决。) +### 6.1.1 三处「数字的第二来源」现状(P3-2 登记) + +| 位置 | 能不能引用常量 | 守卫 | +|---|---|---| +| `presenter/src/host/settings/download.rs` 的行描述 | 能(`format!`) | 已改 | +| `app-iced/src/pages/settings/download.rs` 的 `rows` | 不能:`&'static [(&'static str, &'static str)]` 是编译期字面量,要改运行时生成得动 `Group` 的类型 | 无(`the_downloads_rows_state_semantics…` 只断言 `"1~64"` 这个字符串在,改常量不会红) | +| `setting/keys.rs` 的 `SettingSpec.doc` | 不能:`doc: &'static str`(`spec.rs:183`),`concat!` 只吃字面量、插不进常量 | ✅ `the_key_docs_quote_the_real_limits`(`config.rs`):把「1~64」「出厂 8」「出厂 3」与常量比对,改常量不改文案就红 | + +`SettingSpec.doc` 那一处**不是注释**:`catalog.rs` 会把它当 JSON schema 的 `description` 发出去 +(AI 工具与文档可见),所以它必须有守卫——这也是三处里唯一补上了守卫的一处。 + ## 7. 没验的部分 * **真实网络下的并发/重试行为未验**:守卫读的是 `Semaphore` 的真实许可数与 `attempts_for()` 的