Skip to content

feat(config): support GitHub proxy for release downloads via ureq middleware - #541

Merged
wangl-cc merged 3 commits into
MaaAssistantArknights:mainfrom
lingdiansr:feature/github-proxy
Aug 5, 2026
Merged

feat(config): support GitHub proxy for release downloads via ureq middleware#541
wangl-cc merged 3 commits into
MaaAssistantArknights:mainfrom
lingdiansr:feature/github-proxy

Conversation

@lingdiansr

@lingdiansr lingdiansr commented May 4, 2026

Copy link
Copy Markdown
Contributor

功能说明

支持通过配置文件 cli.toml 中的 github_proxy 字段设置 GitHub 代理前缀,所有通过 ureq 的 GitHub release 下载将被透明改写走代理。

使用方式

# cli.toml
github_proxy = "https://gh-proxy.org/"

实现方式

采用 ureq middleware 方案(感谢 @wangl-cc 的建议),在全局 AGENT 构造时注入 GitHubProxyMiddleware,对 host 为 github.com 且路径含 /releases/download/ 的请求透明改写 URL。覆盖所有通过 AGENT 的下载:maa-core、CLI 自更新、hot-update。

修改文件

  • crates/maa-cli/src/state.rs:新增 rewrite_github_url() + GitHubProxyMiddleware,AGENT 构造时条件注入
  • crates/maa-cli/src/config/cli/mod.rs:新增 github_proxy 配置字段及 github_proxy() 方法
  • crates/maa-cli/config_examples/cli.toml:新增 github_proxy 配置示例及注释

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并给出了一些整体性的反馈:

  • github_proxy() 访问器可以对返回值进行规范化处理(例如使用 trim_end_matches('/')),这样像 apply_github_proxy 这样的调用方就不需要每次都记得自己去移除结尾的斜杠。
  • github_proxy 的测试用例把 std::env::set_var / remove_var 包裹在 unsafe 块中,这是不必要的,而且可能会让人困惑;你可以去掉 unsafe,如果需要的话,把测试标记为串行执行以避免测试之间的相互干扰。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `github_proxy()` accessor could normalize the value (e.g., `trim_end_matches('/')`) so callers like `apply_github_proxy` don't all have to remember to strip trailing slashes themselves.
- The `github_proxy` tests wrap `std::env::set_var`/`remove_var` in `unsafe` blocks, which is unnecessary and may be confusing; you can drop the `unsafe` and, if needed, mark the test as serial to avoid cross-test interference.

## Individual Comments

### Comment 1
<location path="crates/maa-cli/src/installer/maa_core.rs" line_range="34-40" />
<code_context>
+    url.split('/').nth(2) == Some("github.com") && url.contains("/releases/download/")
+}
+
+fn apply_github_proxy(manifest: &mut VersionManifest<core::Details>, proxy: &str) {
+    let proxy = proxy.trim_end_matches('/');
+    for asset in &mut manifest.details.assets {
+        if is_github_release_url(&asset.browser_download_url) {
+            asset.browser_download_url = format!("{proxy}/{}", asset.browser_download_url);
+        }
+        for mirror in &mut asset.mirrors {
+            if is_github_release_url(mirror) {
+                *mirror = format!("{proxy}/{mirror}");
</code_context>
<issue_to_address>
**issue:** 需要防范空 proxy 值,以避免生成无效的 URL。

当 `github_proxy` 被配置为空字符串时,`CLI_CONFIG.github_proxy()` 会返回 `Some("")`,并且在调用 `trim_end_matches('/')` 之后,该值仍然是空的。此时 `apply_github_proxy` 会生成类似 `/https://github.com/...` 的 URL。请在调用该函数前将空的 proxy 视为 `None`,或者在这里当 `proxy` 为空时提前返回,以避免生成无效的 URL。
</issue_to_address>

### Comment 2
<location path="crates/maa-cli/src/config/cli/mod.rs" line_range="47-51" />
<code_context>
+    ///
+    /// Returns the value of the `MAA_GITHUB_PROXY` environment variable if set
+    /// and non-empty, otherwise falls back to the configured value.
+    pub fn github_proxy(&self) -> Option<String> {
+        std::env::var("MAA_GITHUB_PROXY")
+            .ok()
+            .filter(|s| !s.is_empty())
+            .or_else(|| self.github_proxy.clone())
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 规范空配置值,并考虑避免在 `github_proxy` 中进行重复分配。

当前实现会把配置中的 `github_proxy = ""` 当作一个真实的代理(`Some("")`),而环境变量则会忽略空字符串。建议对配置进行规范化,将空字符串视为 `None`,以保持行为一致。另外,这个实现每次调用都会分配一个新的 `String`;如果这是一个热路径,返回 `Option<&str>``Option<Cow<'_, str>>` 可以避免重复克隆。

建议实现:

```rust
    /// Get the GitHub proxy prefix.
    ///
    /// Returns the value of the `MAA_GITHUB_PROXY` environment variable if set
    /// and non-empty, otherwise falls back to the configured value.
    ///
    /// Empty strings from either the environment or the configuration are
    /// treated as if no proxy were configured.
    pub fn github_proxy(&self) -> Option<std::borrow::Cow<'_, str>> {
        // Prefer a non-empty value from the environment (owned string)
        if let Ok(env_val) = std::env::var("MAA_GITHUB_PROXY") {
            if !env_val.is_empty() {
                return Some(std::borrow::Cow::Owned(env_val));
            }
        }

        // Fall back to a non-empty configured value, borrowed to avoid cloning
        self.github_proxy
            .as_deref()
            .filter(|s| !s.is_empty())
            .map(std::borrow::Cow::Borrowed)

```

1. 如果你更喜欢更短的类型名,可以在模块顶部添加 `use std::borrow::Cow;`,然后把函数签名和函数体中的 `std::borrow::Cow<'_, str>` 替换为 `Cow<'_, str>`2. 这个改动会把 `github_proxy` 的返回类型从 `Option<String>` 改为 `Option<Cow<'_, str>>`。所有目前期望 `Option<String>` 的调用点都需要更新:
   - 如果它们只需要借用的字符串,使用 `if let Some(proxy) = cfg.github_proxy() { /* &proxy */ }`- 如果需要拥有所有权的 `String`,调用 `cfg.github_proxy().map(|c| c.into_owned())`。
</issue_to_address>

Sourcery 对开源项目免费——如果你喜欢我们的评审,请考虑分享它们 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据这些反馈改进后续的评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • The github_proxy() accessor could normalize the value (e.g., trim_end_matches('/')) so callers like apply_github_proxy don't all have to remember to strip trailing slashes themselves.
  • The github_proxy tests wrap std::env::set_var/remove_var in unsafe blocks, which is unnecessary and may be confusing; you can drop the unsafe and, if needed, mark the test as serial to avoid cross-test interference.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `github_proxy()` accessor could normalize the value (e.g., `trim_end_matches('/')`) so callers like `apply_github_proxy` don't all have to remember to strip trailing slashes themselves.
- The `github_proxy` tests wrap `std::env::set_var`/`remove_var` in `unsafe` blocks, which is unnecessary and may be confusing; you can drop the `unsafe` and, if needed, mark the test as serial to avoid cross-test interference.

## Individual Comments

### Comment 1
<location path="crates/maa-cli/src/installer/maa_core.rs" line_range="34-40" />
<code_context>
+    url.split('/').nth(2) == Some("github.com") && url.contains("/releases/download/")
+}
+
+fn apply_github_proxy(manifest: &mut VersionManifest<core::Details>, proxy: &str) {
+    let proxy = proxy.trim_end_matches('/');
+    for asset in &mut manifest.details.assets {
+        if is_github_release_url(&asset.browser_download_url) {
+            asset.browser_download_url = format!("{proxy}/{}", asset.browser_download_url);
+        }
+        for mirror in &mut asset.mirrors {
+            if is_github_release_url(mirror) {
+                *mirror = format!("{proxy}/{mirror}");
</code_context>
<issue_to_address>
**issue:** Guard against empty proxy values to avoid generating invalid URLs.

When `github_proxy` is configured as an empty string, `CLI_CONFIG.github_proxy()` returns `Some("")`, and after `trim_end_matches('/')` the value is still empty. `apply_github_proxy` will then produce URLs like `/https://github.com/...`. Please either treat an empty proxy as `None` before calling this function or early-return here when `proxy` is empty to avoid generating invalid URLs.
</issue_to_address>

### Comment 2
<location path="crates/maa-cli/src/config/cli/mod.rs" line_range="47-51" />
<code_context>
+    ///
+    /// Returns the value of the `MAA_GITHUB_PROXY` environment variable if set
+    /// and non-empty, otherwise falls back to the configured value.
+    pub fn github_proxy(&self) -> Option<String> {
+        std::env::var("MAA_GITHUB_PROXY")
+            .ok()
+            .filter(|s| !s.is_empty())
+            .or_else(|| self.github_proxy.clone())
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Normalize empty config values and consider avoiding repeated allocations in `github_proxy`.

This currently treats `github_proxy = ""` in the config as a real proxy (`Some("")`), unlike the env var, which ignores empty strings. Consider normalizing config so empty strings are treated as `None` for consistency. Also, this allocates a new `String` on every call; if it’s on a hot path, returning `Option<&str>` or `Option<Cow<'_, str>>` would avoid repeated cloning.

Suggested implementation:

```rust
    /// Get the GitHub proxy prefix.
    ///
    /// Returns the value of the `MAA_GITHUB_PROXY` environment variable if set
    /// and non-empty, otherwise falls back to the configured value.
    ///
    /// Empty strings from either the environment or the configuration are
    /// treated as if no proxy were configured.
    pub fn github_proxy(&self) -> Option<std::borrow::Cow<'_, str>> {
        // Prefer a non-empty value from the environment (owned string)
        if let Ok(env_val) = std::env::var("MAA_GITHUB_PROXY") {
            if !env_val.is_empty() {
                return Some(std::borrow::Cow::Owned(env_val));
            }
        }

        // Fall back to a non-empty configured value, borrowed to avoid cloning
        self.github_proxy
            .as_deref()
            .filter(|s| !s.is_empty())
            .map(std::borrow::Cow::Borrowed)

```

1. If you prefer shorter type names, you can add `use std::borrow::Cow;` near the top of the module and then change the function signature and body to use `Cow<'_, str>` instead of `std::borrow::Cow<'_, str>`.
2. This change alters the return type of `github_proxy` from `Option<String>` to `Option<Cow<'_, str>>`. All call sites that currently expect an `Option<String>` will need to be updated:
   - If they only need a borrowed string, use `if let Some(proxy) = cfg.github_proxy() { /* &proxy */ }`.
   - If they require an owned `String`, call `cfg.github_proxy().map(|c| c.into_owned())`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/maa-cli/src/installer/maa_core.rs Outdated
Comment thread crates/maa-cli/src/config/cli/mod.rs Outdated
@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.87097% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.34%. Comparing base (dc7d1a9) to head (c49d5c2).

Files with missing lines Patch % Lines
crates/maa-cli/src/state.rs 86.60% 14 Missing and 1 partial ⚠️
crates/maa-cli/src/installer/maa_core.rs 16.66% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #541      +/-   ##
==========================================
+ Coverage   72.07%   72.34%   +0.27%     
==========================================
  Files          72       74       +2     
  Lines        6679     6813     +134     
  Branches     6679     6813     +134     
==========================================
+ Hits         4814     4929     +115     
- Misses       1523     1538      +15     
- Partials      342      346       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@wangl-cc

Copy link
Copy Markdown
Member

我个人不是很喜欢使用使用环境变量来做配置,因为实现和测试起来比较麻烦。我想知道其他 CLI 使用类似的环境变量来做 GITHUB 的代理吗? 此外如果只是对于 Core releases 来做的话,是不是放在 Core 这个 section 里面更合适?

@lingdiansr

Copy link
Copy Markdown
Contributor Author

我个人不是很喜欢使用使用环境变量来做配置,因为实现和测试起来比较麻烦。我想知道其他 CLI 使用类似的环境变量来做 GITHUB 的代理吗? 此外如果只是对于 Core releases 来做的话,是不是放在 Core 这个 section 里面更合适?

其他cli确实每怎么见过专给github设置代理的变量的,因为都是外国开发者没怎么考虑过,都使用通用的http_proxy。
我查ai说这种方式能划掉unsafe,但是还没试过,之后研究下。

  #[test]
  fn env_via_subprocess() {
      let exe = std::env::current_exe().unwrap();
      let output = std::process::Command::new(exe)
          .env("MAA_GITHUB_PROXY", "https://env/")
          .arg("--test-threads=1")
          .arg("subprocess_github_proxy")
          .output()
          .unwrap();
      assert!(output.status.success());
  }

  #[test]
  fn subprocess_github_proxy() {
      // 这个测试只在子进程中运行
      let config = CLIConfig::default();
      assert_eq!(config.github_proxy(), Some("https://env".into()));
  }

我不是很喜欢一直开启代理软件。
现有mirror的稳定性和可用性感觉不是很行,我从来没匹配到过mirror,不知道是不是我github本地反代的原因。
我测试了一段时间发现通用代理和本地反代都不如gh-proxy.com稳定😂。
设置范围的话,我主要使用的代理站https://gh-proxy.com/ 基本上能覆盖本项目所有涉及的url,可以按照mirror->gh proxy->github 的层次fallback。可以与一些涉及手动指定url的配置合并。

@wangl-cc

wangl-cc commented May 15, 2026

Copy link
Copy Markdown
Member

我觉得可以提供这个功能,不过我倾向于要么用环境变量,要么用设置二选一,设置测试起来比较方便。此外我觉得,其实这个可以用 ureq 的 milldeware 来做,可能这样比较方便, 而不是在设置里面到处打补丁。

@lingdiansr
lingdiansr force-pushed the feature/github-proxy branch from 327b543 to f452e5c Compare August 4, 2026 18:43
@lingdiansr lingdiansr changed the title feat(config): support GitHub proxy via MAA_GITHUB_PROXY env var feat(config): support GitHub proxy for release downloads via ureq middleware Aug 4, 2026
…dleware

Add GitHubProxyMiddleware in state.rs that transparently rewrites
github.com release download URLs through the configured proxy prefix.
The middleware is injected into the global AGENT at construction time,
covering all downloads: maa-core install/update, CLI self-update,
and hot-update.

Configuration is via cli.toml only:
  github_proxy = "https://gh-proxy.org/"
@lingdiansr
lingdiansr force-pushed the feature/github-proxy branch from 592fc8e to 73bae7b Compare August 4, 2026 18:51
@lingdiansr

Copy link
Copy Markdown
Contributor Author

@wangl-cc 已按照你的建议用 ureq middleware 重新实现了:

  • state.rs 中新增 GitHubProxyMiddleware,注入到全局 AGENT 构造中,对所有 github.com/*/releases/download/* 请求透明改写 URL
  • 仅保留配置文件方式(cli.toml 中的 github_proxy 字段),移除了 MAA_GITHUB_PROXY 环境变量
  • 删除了之前在 maa_core.rs 中手动 patch manifest URL 的 apply_github_proxy / is_github_release_url

AGENT previously read the proxy from CLI_CONFIG at construction time,
which forced CLI_CONFIG initialization on any agent user (e.g. config
import tests). Under the core_installer-only feature set, the sample
cli.toml fails to parse (backend = "libgit2" without the git2 feature),
panicking the whole test run.

Read the proxy from a dedicated GITHUB_PROXY LazyLock that treats parse
errors as disabled, keeping AGENT usable in all feature combinations.
@wangl-cc

wangl-cc commented Aug 5, 2026

Copy link
Copy Markdown
Member

LGTM,感谢贡献!!

@wangl-cc
wangl-cc added this pull request to the merge queue Aug 5, 2026
Merged via the queue into MaaAssistantArknights:main with commit de9c4d9 Aug 5, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants