Files
Unlucky-Launcher/docs/download-settings.md
T
worker-dlmsettings 94f56f9d9c fix(m193): 复审 r1 三条 P2——版本史还原、retry_limit 0 有诚实语义、装配点措辞更正
- 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 = 只试一次)。
2026-11-09 05:24:12 +08:00

152 lines
11 KiB
Markdown
Raw Blame History

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