- 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 = 只试一次)。
152 lines
11 KiB
Markdown
152 lines
11 KiB
Markdown
# 下载设置到底改了谁(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)。
|