feat(download): 把 download.proxy_mode / download.proxy_url 接进代理三态(M295)

台账 P26:两个键引擎侧零消费者——`http::build_client` 只设 UA / 连接超时 / 重定向 / 默认头,
从不 `.proxy()`。设置页画着三档 chip 与一个地址框,改它对任何一次下载都不生效(`system` 那一
档「碰巧能用」只是因为 reqwest 默认读环境变量,不是这段代码做的)。

- core::download::proxy(新):`ProxySetting::from_raw` 是**唯一**翻译层,三档落到 client 上:
  `none` ⇒ `.no_proxy()`(真直连,连 `HTTP_PROXY` 也不读)/ `system` ⇒ 不调任何代理 API
  (跟随环境变量 = M295 之前的行为)/ `manual` ⇒ `.proxy(Proxy::all(url))`;未知值 ⇒ `none`。
- 两条决定写进模块头:`proxy_url` 在非 manual 档**忽略**(不校验、不进结构,值原样留在文件
  里);`manual` + 空地址 ⇒ `system`(设置页那行文案一直这么承诺)。
- 非法地址不穿透:`Proxy::all` 解析失败 ⇒ 有界错误(`InvalidRequest` + 带原串的文案),不
  panic;`DownloadEngine::new` 回落**直连**重建(不用 `reqwest::Client::new()`——那会跟随环境
  变量代理,回落就不确定),原因留在 `proxy_problem()`,CLI 的 `download::engine` 打一行
  stderr;`config().proxy` 不撒谎(仍是用户写的 `manual`)。
- `EngineConfig.proxy` 默认 `System`(=「没读设置」的档:auth 登录 / JDK 下载页 / 地区探测都
  直接用它,保持老行为),`DownloadTuning.proxy` 默认 `Direct`(与 schema 的 `none` 一致)。
- 两键进 `RawTuning`(与 `gh_proxy` 同形),presenter 的 `tuning_of` 与 CLI 的 `tuning` 各加
  两行;两个宿主都是完整字段字面量 ⇒ 「忘了读」是编译错误。`summary()` 加 `代理=…` 一档。
- 判据是「请求真的经过代理」:新夹具 `FakeProxy`(本地 TcpListener)+ 目标 `.invalid`(解析不
  了)⇒ 拿到正文只可能来自代理,且代理记到的请求行是**绝对 URI**。三条用例:manual 真走代理
  / direct 无视环境变量而 system 跟随(同一份 `HTTP_PROXY`)/ 非法地址回落直连不 panic。
  变异 M17-M22 记在 docs/download-settings.md §6。
- 行为变化(doc §5.2 写明):`none` 从「碰巧跟随环境变量」变成真直连,靠 `HTTP_PROXY` 上网的
  用户要选一次 `system`。发布版 GUI 的「下载 JDK」页仍不读这两个键(finding 已开)。
- 门禁:四 crate `check --all-targets` / 测试(3201 passed, 0 failed)/ `clippy -D warnings` /
  `fmt --check`;另跑了 `check --workspace --all-targets`。判据用 `$?` 与 `test result:` 行验。
This commit is contained in:
AskaEth committed 2026-11-13 10:43:09 +08:00
1 parent c731d3fa54
commit d584a2c42e
16 files changed
+1000 -56

No files matched your search

+14 -10
View File
@@ -40,25 +40,29 @@ pub const USER_AGENT: &str = concat!(
" (+https://github.com/AskaEth/Unlucky-Launcher)"
);
/// 构造共享的 `reqwest::Client`(连接池、超时、重定向策略一次定好)。
/// 构造共享的 `reqwest::Client`(连接池、超时、重定向策略、代理一次定好)。
///
/// **代理由配置决定,不在这里解释字符串**:`cfg.proxy` 是 `download.proxy_mode` / `proxy_url` 的
/// 翻译结论(唯一翻译层是 [`super::proxy`]),三档各自落到 `.no_proxy()` / 什么都不做 /
/// `.proxy(Proxy::all(url))`。地址解析不了时返回**有界错误**(不 panic)——装配点
/// (`DownloadEngine::new`)据此回落直连并把原因留在引擎上,见 [`super::proxy`] 模块头。
pub fn build_client(cfg: &EngineConfig) -> Result<reqwest::Client, DownloadError> {
let mut headers = HeaderMap::new();
headers.insert(
ACCEPT_ENCODING,
reqwest::header::HeaderValue::from_static("identity"),
);
reqwest::Client::builder()
let builder = reqwest::Client::builder()
.user_agent(cfg.user_agent.clone())
.connect_timeout(cfg.connect_timeout)
.redirect(Policy::limited(MAX_REDIRECTS))
.default_headers(headers)
.build()
.map_err(|err| {
DownloadError::new(
DownloadErrorKind::Network,
format!("构造 HTTP 客户端失败:{err}"),
)
})
.default_headers(headers);
cfg.proxy.apply(builder)?.build().map_err(|err| {
DownloadError::new(
DownloadErrorKind::Network,
format!("构造 HTTP 客户端失败:{err}"),
)
})
}
/// **每一条 `send()` 的唯一上界构造点**:`transfer.rs` 的单连接请求、`chunked.rs` 的分块请求、