Files
Unlucky-Launcher/docs/download-settings.md
T
worker-dlmsettings 54dde7f494 fix(m193): 复审 r2——台账三处失效引用、M39 注释逐字还原、schema doc 数字补守卫
- 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 里那个游离反引号删掉。
2026-11-09 05:24:12 +08:00

12 KiB
Raw Blame History

下载设置到底改了谁(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 相同,但改它无效) ✅ 接进引擎,范围 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)
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. 唯一来源:默认值谁说了算

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_contract_is_single_sourced(download/config.rs,它合并了原先的「默认值相等」与 「区间同源」两条)让「改一处另一个不变」直接变红——这是防 8 vs 64 复发的唯一手段。 另有 the_key_docs_quote_the_real_limits 钉住 schema 描述里的数字(见 §6.1 的第三处字面量)。

3. 接线链(宿主 → 引擎)

config/launcher.json
  download.concurrency ─┐
  download.retry_limit ─┼→ presenter::host::settings::download::tuning_of
  download.gh_proxy ────┘        │  DownloadTuning::from_settings(默认值/夹取/回落都在 core)
                                 ↓
                     shared::set_tuning(<cache 根>, tuning)   变了吗?没变就什么都不做
                                 ↓ 变了 → 淘汰该根的登记项
                     shared::engine_for(<cache 根>) → 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)。

调优按数据根存(与引擎登记处同键),不是进程级:同一进程里多个数据根时互不串味。

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

值 含义 效果
空串(出厂) 没有覆盖,跟随内置默认 镜像优先/国内判定时套 mirrors.json 里的 gh-proxy.com
off(也认 none/direct/disable/disabled/no) 明确关掉 一个 gh-proxy 候选都不生成
其它非空 覆盖前缀 用它替换内置前缀(类别仍只在镜像优先/国内判定时启用)

空串 ≠ 关:「我还没决定」和「我明确不要」是两件事。设置页的说明文案(presenter 行 + iced 行) 两档必须都写出来——这是最容易让用户以为「关掉了、其实还在套前缀」的地方。

边界:非法值(负数、超上限、手改坏值)一律回落出厂默认,不报错、不阻断下载;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)

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_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 描述里的「164」改成「199」(第三处字面量) download::config::tests::the_key_docs_quote_the_real_limits 红

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,仍是已知的第二真相源,等真控件一并解决。)

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() 的 配置读点;「2 条真连接同时下载时服务器只看到 2 条」和「某个源真的被试 3 次」没有在真网络下跑过 (离线回环能验连接复用,验不了限流的端到端时序)。
  • macOS / Linux 未验:本轮只在 Windows 上编译与跑测试。
  • download.gh_proxy 的端到端未验:那个键由 M197 登记,尚未合入 dev;setting::document 只认 已登记的键,所以「写文件 ⇒ 读出来 ⇒ 进候选链」这一段在 dev 上跑不通(core 侧「调优 → 候选链」 已守)。M197 合入后应补一条宿主侧端到端用例。
  • iced 设置页仍然不是可改控件:它画的是语义行(默认值/区间/三态),真值不显示——因为 iced 侧 还没有「presenter 设置快照 → app → Groups.download」的数据通路(另立 mission,见 finding)。