Skip to content

Fix unstable GitHub access and add mirror-to-direct fallback - #23

Merged
jingjiangze merged 10 commits into
mainfrom
fix/stable-github-fallback
Oct 14, 2026
Merged

jingjiangze merged 10 commits into
mainfrom
fix/stable-github-fallback

Conversation

@jingjiangze

@jingjiangze jingjiangze commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

审计结论

  1. GitHub 主站与关键资源域被静态写入 hosts 后,线路波动会被放大为整机访问故障。此前本机记录中存在 GitHub IP 连续 HTTP 000/超时;单轮探测成功不足以证明地址长期可用。
  2. gh.mjs 虽然并发竞速镜像,但镜像全部失败时原先直接退出;克隆也缺少官方源直连的最终回退。
  3. 镜像没有返回首字节时,原竞速流程可能反复重启入口,直到总超时才结束。
  4. 动态清单过期后串行抓取多个源,会累加故障超时。
  5. fake-IP 环境下 accel.mjs 会跳过测速;若旧版本留下 GitHub 静态钉,该早退路径可能一直保留坏钉。

改动

  • 不改 Clash 配置;GitHub 主站和关键资源域在代码级禁止写入静态 hosts 固定。
  • fake-IP 检测提前退出时,仍会尝试只清除工具管理块内的 GitHub 核心域旧钉,保留其他域的既有固定;带回归自测。
  • 下载:健康镜像并发竞速 → 无可用镜像、体检失败或竞速失败时,官方 URL 无代理直连重试。直连清除子进程代理环境变量并使用 --noproxy '*';成功前不提交输出文件。
  • 克隆:镜像探测/重试失败后,清除代理环境变量和 Git 代理配置,以官方 URL 直连重试,不添加镜像重写。
  • 每个下载镜像都有明确的重试预算与耗尽状态;清单源并行刷新。
  • 如果克隆目标目录在运行前已存在,拒绝覆盖或删除。
  • 新增 Windows CI,执行语法检查及离线自测。

验证

  • 配置 JSON 已在提交前解析。
  • Windows CI 的语法检查和离线自测通过(最新提交也会触发同一工作流)。
  • 未对浏览器或 Clash 做配置变更。

范围边界

此方案能移除本工具制造的坏静态钉,并为本工具执行的下载/克隆补齐镜像到官方直连的回退。但在保持 Clash 不变、浏览器 GitHub 路由仍为 DIRECT 的前提下,浏览器页面无法被本仓库脚本透明切换到镜像;任何代码也无法保证运营商链路、DNS 或 GitHub 上游永不故障。

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

审阅者指南

本 PR 通过禁止 GitHub 核心域静态 hosts 固定,降低自适应 IP 钉造成的系统级故障;同时重构下载和克隆失败路径,采用有限预算的镜像竞速并在镜像不可用时清除代理后直连官方 GitHub,辅以目标目录保护、并行清单刷新和 Windows 离线 CI 验证。

镜像下载失败后回退到直连的时序图

sequenceDiagram
    participant User
    participant gh as gh.mjs
    participant Manifest as MirrorManifest
    participant Mirrors
    participant GitHub
    participant File as OutputFile

    User->>gh: race(url, name)
    gh->>Manifest: loadManifest()
    Manifest-->>gh: manifest sources in parallel
    gh->>Mirrors: checkAll()
    Mirrors-->>gh: healthy entries
    gh->>Mirrors: startCurl() concurrently
    alt Mirror succeeds
        Mirrors-->>gh: complete file
        gh->>File: commit successful output
    else Mirrors unavailable or exhausted
        gh->>gh: proxyFreeEnv()
        gh->>GitHub: curl with --noproxy *
        alt Direct download succeeds
            GitHub-->>gh: complete temporary file
            gh->>File: rename temporary file
        else Direct download fails
            gh-->>User: exit with failure
        end
    end
Loading

镜像克隆失败后回退到直连的时序图

sequenceDiagram
    participant User
    participant gh as gh.mjs
    participant Mirrors
    participant GitHub
    participant Target as CloneTarget

    User->>gh: clone(args)
    gh->>gh: cloneOutputDir(repo, rest)
    alt Target already exists
        gh-->>User: reject without overwrite
    else Target is absent
        gh->>Mirrors: probeRefs(repo)
        alt Mirror clone succeeds
            Mirrors-->>gh: clone complete
            gh->>Target: retain cloned directory
        else Mirror probing or retries fail
            gh->>gh: proxyFreeEnv()
            gh->>GitHub: git clone with cleared proxy and no insteadOf
            alt Direct clone succeeds
                GitHub-->>gh: clone complete
                gh->>Target: retain cloned directory
            else Direct clone fails
                gh->>Target: cleanupFailedClone(targetDir)
                gh-->>User: exit with failure
            end
        end
    end
Loading

有限镜像尝试的状态图

stateDiagram-v2
    [*] --> Probing
    Probing --> Racing: healthy entries found
    Probing --> DirectFallback: no healthy entries or health check fails
    Racing --> Racing: retry within entry budget
    Racing --> Exhausted: first-byte or speed budget exhausted
    Racing --> Success: complete non-empty file
    Exhausted --> Racing: another non-exhausted entry
    Exhausted --> DirectFallback: all entries exhausted
    DirectFallback --> Success: official direct retry succeeds
    DirectFallback --> Failed: retry budget exhausted
    Success --> [*]
    Failed --> [*]
Loading

文件级变更

Change Details Files
排除 GitHub 核心域的静态 hosts 固定,并增加离线安全回归验证。
  • 新增不可固定域名集合,运行时跳过写入并记录安全原因。
  • 扩展自测以验证核心域保护规则。
  • 同步更新稳定性策略、域名覆盖范围和工具边界说明。
accel.mjs
README.md
为下载流程增加镜像失败后的官方 GitHub 无代理直连回退,并改进竞速器终态管理。
  • 并行刷新镜像清单源,减少串行超时累加。
  • 为镜像入口增加首字节、停滞和重试预算耗尽状态,避免无限重启。
  • 官方回退清除代理环境变量并使用 curl 无代理选项,先写临时文件且成功后才提交输出。
  • 增加直连下载相关离线自测。
gh.mjs
为 Git 克隆增加官方直连回退及目标目录保护。
  • 镜像探测和体检失败后清除代理环境变量与 Git 代理设置,直接克隆官方 URL。
  • 官方回退不注入镜像 URL 重写,并按配置执行有限重试。
  • 改进克隆目标目录解析,运行前已存在时拒绝覆盖。
  • 失败清理仅作用于本次运行确认不存在的目标目录。
gh.mjs
增加 Windows CI 以执行 Node.js 语法检查和离线回归测试。
  • 在 Windows latest、Node.js 22 环境运行全部模块语法检查。
  • 执行各模块 selftest 及中间件测试。
.github/workflows/test.yml
补充配置以支持直连回退和新的重试策略。
  • 更新运行时配置项以覆盖直连回退、竞速和克隆重试行为。
  • 提交前验证配置 JSON 可解析。
config.json

提示和命令

与 Sourcery 互动

  • 触发新的审阅: 在拉取请求中评论 @sourcery-ai review。
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 根据审阅评论创建 GitHub issue: 回复审阅评论,请 Sourcery 根据该评论创建 issue。你也可以回复 @sourcery-ai issue,根据该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,随时(重新)生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中你希望生成摘要的位置写入 @sourcery-ai summary,即可随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,随时(重新)生成摘要。
  • 生成审阅者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 驳回所有 Sourcery 审阅: 在拉取请求中评论 @sourcery-ai dismiss,即可驳回所有现有的 Sourcery 审阅。如果你想从头开始进行新的审阅,这尤其有用——别忘了评论 @sourcery-ai review 以触发新的审阅!

自定义使用体验

访问你的控制面板以:

  • 启用或停用审阅功能,例如 Sourcery 生成的拉取请求摘要、审阅者指南等。
  • 更改审阅语言。
  • 添加、删除或编辑自定义审阅说明。
  • 调整其他审阅设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 通过禁止 GitHub 核心域静态 hosts 固定,降低自适应 IP 钉造成的系统级故障;同时重构下载和克隆失败路径,采用有限预算的镜像竞速并在镜像不可用时清除代理后直连官方 GitHub,辅以目标目录保护、并行清单刷新和 Windows 离线 CI 验证。

Sequence diagram for mirror-to-direct download fallback

sequenceDiagram
    participant User
    participant gh as gh.mjs
    participant Manifest as MirrorManifest
    participant Mirrors
    participant GitHub
    participant File as OutputFile

    User->>gh: race(url, name)
    gh->>Manifest: loadManifest()
    Manifest-->>gh: manifest sources in parallel
    gh->>Mirrors: checkAll()
    Mirrors-->>gh: healthy entries
    gh->>Mirrors: startCurl() concurrently
    alt Mirror succeeds
        Mirrors-->>gh: complete file
        gh->>File: commit successful output
    else Mirrors unavailable or exhausted
        gh->>gh: proxyFreeEnv()
        gh->>GitHub: curl with --noproxy *
        alt Direct download succeeds
            GitHub-->>gh: complete temporary file
            gh->>File: rename temporary file
        else Direct download fails
            gh-->>User: exit with failure
        end
    end
Loading

Sequence diagram for mirror-to-direct clone fallback

sequenceDiagram
    participant User
    participant gh as gh.mjs
    participant Mirrors
    participant GitHub
    participant Target as CloneTarget

    User->>gh: clone(args)
    gh->>gh: cloneOutputDir(repo, rest)
    alt Target already exists
        gh-->>User: reject without overwrite
    else Target is absent
        gh->>Mirrors: probeRefs(repo)
        alt Mirror clone succeeds
            Mirrors-->>gh: clone complete
            gh->>Target: retain cloned directory
        else Mirror probing or retries fail
            gh->>gh: proxyFreeEnv()
            gh->>GitHub: git clone with cleared proxy and no insteadOf
            alt Direct clone succeeds
                GitHub-->>gh: clone complete
                gh->>Target: retain cloned directory
            else Direct clone fails
                gh->>Target: cleanupFailedClone(targetDir)
                gh-->>User: exit with failure
            end
        end
    end
Loading

State diagram for bounded mirror attempts

stateDiagram-v2
    [*] --> Probing
    Probing --> Racing: healthy entries found
    Probing --> DirectFallback: no healthy entries or health check fails
    Racing --> Racing: retry within entry budget
    Racing --> Exhausted: first-byte or speed budget exhausted
    Racing --> Success: complete non-empty file
    Exhausted --> Racing: another non-exhausted entry
    Exhausted --> DirectFallback: all entries exhausted
    DirectFallback --> Success: official direct retry succeeds
    DirectFallback --> Failed: retry budget exhausted
    Success --> [*]
    Failed --> [*]
Loading

File-Level Changes

Change Details Files
排除 GitHub 核心域的静态 hosts 固定,并增加离线安全回归验证。
  • 新增不可固定域名集合,运行时跳过写入并记录安全原因。
  • 扩展自测以验证核心域保护规则。
  • 同步更新稳定性策略、域名覆盖范围和工具边界说明。
accel.mjs
README.md
为下载流程增加镜像失败后的官方 GitHub 无代理直连回退,并改进竞速器终态管理。
  • 并行刷新镜像清单源,减少串行超时累加。
  • 为镜像入口增加首字节、停滞和重试预算耗尽状态,避免无限重启。
  • 官方回退清除代理环境变量并使用 curl 无代理选项,先写临时文件且成功后才提交输出。
  • 增加直连下载相关离线自测。
gh.mjs
为 Git 克隆增加官方直连回退及目标目录保护。
  • 镜像探测和体检失败后清除代理环境变量与 Git 代理设置,直接克隆官方 URL。
  • 官方回退不注入镜像 URL 重写,并按配置执行有限重试。
  • 改进克隆目标目录解析,运行前已存在时拒绝覆盖。
  • 失败清理仅作用于本次运行确认不存在的目标目录。
gh.mjs
增加 Windows CI 以执行 Node.js 语法检查和离线回归测试。
  • 在 Windows latest、Node.js 22 环境运行全部模块语法检查。
  • 执行各模块 selftest 及中间件测试。
.github/workflows/test.yml
补充配置以支持直连回退和新的重试策略。
  • 更新运行时配置项以覆盖直连回退、竞速和克隆重试行为。
  • 提交前验证配置 JSON 可解析。
config.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@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.

嗨——我发现了 1 个问题

AI 代理提示
请处理本次代码审查中的评论:

## 单条评论

### 评论 1
<location path="gh.mjs" line_range="485" />
<code_context>
+        reason: 'safety-no-pin:GitHub 核心域保留动态 DNS,避免间歇性 IP 固定影响主站/资源' }
+      log('保护性跳过 ', dom.padEnd(28), '不写入 hosts 固定(使用动态 DNS)')
+      continue
+    }
+    if (CFG.deny.indexOf(dom) >= 0) continue
     const conf = CFG.domains[dom] || {}
</code_context>
<issue_to_address>
**其他克隆会被删除**

如果其他进程在初次检查确认目标不存在之后创建了目标目录或完成了克隆,而当前克隆尝试随后失败,`cleanupFailedClone` 只根据 `targetDir` 当前是否存在就递归删除它,因此会删除其他进程的数据或已完成的仓库。

跟踪本次调用创建的目标目录,并且只删除本次调用自身未完成的克隆。

`gh.mjs:519`、`gh.mjs:692` 处也有此问题。
</issue_to_address>

Sourcery 评估

需要人工审查。 有 1 个问题需要优先处理;此外,当镜像尝试失败时,下载和克隆会有意在清除代理环境变量和 Git 代理设置后直接发往 GitHub,从而绕过这些请求原本配置的代理。回滚可以阻止今后再直接回退,但无法撤销已经绕过代理发出的请求。

阻塞性问题:gh.mjs:485


Sourcery 对开源项目免费——如果您喜欢我们的审查,请考虑分享 ✨
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="gh.mjs" line_range="485" />
<code_context>
+        reason: 'safety-no-pin:GitHub 核心域保留动态 DNS,避免间歇性 IP 固定影响主站/资源' }
+      log('保护性跳过 ', dom.padEnd(28), '不写入 hosts 固定(使用动态 DNS)')
+      continue
+    }
+    if (CFG.deny.indexOf(dom) >= 0) continue
     const conf = CFG.domains[dom] || {}
</code_context>
<issue_to_address>
**Other clones are deleted**

When another process creates or completes the destination after the initial absence check and this clone attempt fails, `cleanupFailedClone` recursively removes `targetDir` based only on its current existence, so it deletes the other process's data or completed repository.

Track which destination this invocation created and only remove that invocation's own partial clone.

Also at `gh.mjs:519`, `gh.mjs:692`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and when mirror attempts fail, downloads and clones are deliberately sent to GitHub with proxy environment variables and Git proxy settings cleared, bypassing any configured proxy for those requests. Reverting stops future direct fallbacks, but cannot undo requests already made outside that proxy.

Blocking findings: gh.mjs:485


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread gh.mjs
function cleanupFailedClone(targetDir) {
// Called only after the caller verified the destination did not exist before this run.
try { if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true }) } catch (e) {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 高 · 其他克隆会被删除

如果其他进程在初次检查确认目标不存在之后创建了目标目录或完成了克隆,而当前克隆尝试随后失败,cleanupFailedClone 只根据 targetDir 当前是否存在就递归删除它,因此会删除其他进程的数据或已完成的仓库。

跟踪本次调用创建的目标目录,并且只删除本次调用自身未完成的克隆。

gh.mjs:519、gh.mjs:692 处也有此问题。

给 AI 代理的提示
在 `gh.mjs` 第 485 行:

**其他克隆会被删除**

如果其他进程在初次检查确认目标不存在之后创建了目标目录或完成了克隆,而当前克隆尝试随后失败,`cleanupFailedClone` 只根据 `targetDir` 当前是否存在就递归删除它,因此会删除其他进程的数据或已完成的仓库。

跟踪本次调用创建的目标目录,并且只删除本次调用自身未完成的克隆。

`gh.mjs:519`、`gh.mjs:692` 处也有此问题。
Original comment in English

🟠 High · Other clones are deleted

When another process creates or completes the destination after the initial absence check and this clone attempt fails, cleanupFailedClone recursively removes targetDir based only on its current existence, so it deletes the other process's data or completed repository.

Track which destination this invocation created and only remove that invocation's own partial clone.

Also at gh.mjs:519, gh.mjs:692.

Prompt for AI agents
In `gh.mjs` at line 485:

**Other clones are deleted**

When another process creates or completes the destination after the initial absence check and this clone attempt fails, `cleanupFailedClone` recursively removes `targetDir` based only on its current existence, so it deletes the other process's data or completed repository.

Track which destination this invocation created and only remove that invocation's own partial clone.

Also at `gh.mjs:519`, `gh.mjs:692`.

@jingjiangze
jingjiangze merged commit 6d8f09e into main Oct 14, 2026
3 checks passed
@jingjiangze
jingjiangze deleted the fix/stable-github-fallback branch October 14, 2026 09:28
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.

1 participant