- 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)。
8.3 KiB
下载设置到底改了谁(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. 唯一来源:默认值谁说了算
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. 接线链(宿主 → 引擎)
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)。
调优按数据根存(与引擎登记处同键),不是进程级:同一进程里多个数据根时互不串味。
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)。