From b4fe909087171d70d4d55ecacdd96a3e4c888268 Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:48:54 +0800 Subject: [PATCH] feat: route app updates through Gitee mirror --- README.md | 2 +- README_ZH.md | 2 +- cmd/bootagent-desktop/main_wails.go | 61 ++++++++++++++++++++++-- cmd/bootagent-desktop/main_wails_test.go | 35 ++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6ff21779..d91afc08 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ BootAgent is a local desktop workspace for AI coding Agents. It turns a fresh ma - Imports and exports Providers, Profiles, and selected MCP servers. API keys and MCP secrets are excluded by default; password-encrypted or explicitly confirmed plaintext export is also available. - Discovers MCP servers from initialized Claude Code, Codex, OpenCode, Kilo CLI, and Hermes installations, and applies selected servers across them from the MCP Registry. Scanning runs in the background; edits are explicit and local. - Creates backups, writes atomically, and keeps credentials in private local storage. The latest three historical versions are kept per Profile, Provider, MCP, Agent configuration target, and Skill; backups live under `~/.bootagent/backup` and the per-target count can be changed in Settings. -- Checks for BootAgent updates and installs release artifacts through the built-in updater. +- Checks for BootAgent updates and installs release artifacts through the built-in updater. When the domestic mirror setting is enabled, update checks and downloads use the Gitee mirror; otherwise they use GitHub. ## Supported Agents diff --git a/README_ZH.md b/README_ZH.md index aa4ed723..94cd35dd 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -27,7 +27,7 @@ BootAgent 是一个本地桌面工作台,用来统一管理 AI 编程 Agent。 - 导入和导出 Provider、Profile 以及选中的 MCP 服务器;默认不导出 API Key 和 MCP 秘密,也支持密码加密或明确确认后的明文导出。 - 从已初始化的 Claude Code、Codex、OpenCode、Kilo CLI 和 Hermes 中发现 MCP 服务器,并在 MCP Registry 页面选择同步目标。扫描在后台进行,编辑必须显式应用并只写入本机。 - 创建备份、原子写入,并将凭据保存在本机私有存储中。Profile、Provider、MCP、Agent 配置目标和 Skill 分别保留最近 3 个历史版本;备份统一放在 `~/.bootagent/backup`,可在“设置”中修改每个目标的保留数量。 -- 检查 BootAgent 更新,并通过内置更新器安装发行包。 +- 检查 BootAgent 更新,并通过内置更新器安装发行包。开启国内镜像设置时,更新检查和下载使用 Gitee 镜像;否则使用 GitHub。 ## 支持的 Agent diff --git a/cmd/bootagent-desktop/main_wails.go b/cmd/bootagent-desktop/main_wails.go index facf52ca..ec865c63 100644 --- a/cmd/bootagent-desktop/main_wails.go +++ b/cmd/bootagent-desktop/main_wails.go @@ -5,6 +5,8 @@ package main import ( "context" _ "embed" + "errors" + "io" "log/slog" "os" "runtime" @@ -27,12 +29,51 @@ import ( //go:embed appicon.png var appIcon []byte -func configureUpdater(appInstance *application.App) binding.UpdateBackend { +const updateMirrorMetadata = "bootagent.update.mirror" + +type updateProvider struct { + official, mirror updater.Provider + preferMirror func(context.Context) bool +} + +func (p updateProvider) Name() string { return "bootagent-release" } + +func (p updateProvider) Check(ctx context.Context, request updater.CheckRequest) (*updater.Release, error) { + mirror := p.preferMirror(ctx) + provider := p.official + if mirror { + provider = p.mirror + } + release, err := provider.Check(ctx, request) + if release != nil { + if release.Metadata == nil { + release.Metadata = make(map[string]any) + } + release.Metadata[updateMirrorMetadata] = mirror + } + return release, err +} + +func (p updateProvider) Download(ctx context.Context, release *updater.Release, dst io.Writer, progress func(int64, int64)) error { + if release == nil || release.Metadata == nil { + return errors.New("update release source is missing") + } + mirror, ok := release.Metadata[updateMirrorMetadata].(bool) + if !ok { + return errors.New("update release source is invalid") + } + if mirror { + return p.mirror.Download(ctx, release, dst, progress) + } + return p.official.Download(ctx, release, dst, progress) +} + +func configureUpdater(appInstance *application.App, core *app.UseCases) binding.UpdateBackend { current := version.UpdaterVersion() if current == "" { return nil } - provider, err := github.New(github.Config{ + providerConfig := github.Config{ Repository: "MaimoryLab/BootAgent", ChecksumAsset: "SHA256SUMS", // Every release ships platform-specific installers plus one OTA .zip. @@ -42,11 +83,23 @@ func configureUpdater(appInstance *application.App) binding.UpdateBackend { // 30-second Client.Timeout, which bounds the body transfer too and so // made the update impossible on a slow link. See NewUpdateHTTPClient. HTTPClient: binding.NewUpdateHTTPClient(), - }) + } + official, err := github.New(providerConfig) if err != nil { slog.Error("BootAgent updater provider is unavailable", "error", err) return nil } + providerConfig.Repository = "maimory/BootAgent" + providerConfig.BaseURL = "https://gitee.com/api/v5" + mirror, err := github.New(providerConfig) + if err != nil { + slog.Error("BootAgent mirror updater provider is unavailable", "error", err) + return nil + } + provider := updateProvider{official: official, mirror: mirror, preferMirror: func(ctx context.Context) bool { + settings, err := core.Settings(ctx) + return err == nil && settings.PreferMirror + }} if err := appInstance.Updater.Init(updater.Config{ CurrentVersion: current, Providers: []updater.Provider{provider}, @@ -236,7 +289,7 @@ func main() { ShouldQuit: func() bool { return quitting.Load() }, }) appInstance.OnShutdown(func() { _ = core.CloseConversion() }) - updateBackend := configureUpdater(appInstance) + updateBackend := configureUpdater(appInstance, core) if updateBackend != nil { updateBackend = quitAwareUpdater{UpdateBackend: updateBackend, quitting: &quitting, restarting: &atomic.Bool{}} } diff --git a/cmd/bootagent-desktop/main_wails_test.go b/cmd/bootagent-desktop/main_wails_test.go index bf8e724b..3d2f1893 100644 --- a/cmd/bootagent-desktop/main_wails_test.go +++ b/cmd/bootagent-desktop/main_wails_test.go @@ -3,14 +3,49 @@ package main import ( + "bytes" "context" "errors" + "io" "sync/atomic" "testing" "github.com/MaimoryLab/BootAgent/internal/binding" + "github.com/wailsapp/wails/v3/pkg/updater" ) +type updateProviderFake struct { + checks, downloads int +} + +func (p *updateProviderFake) Name() string { return "fake" } +func (p *updateProviderFake) Check(context.Context, updater.CheckRequest) (*updater.Release, error) { + p.checks++ + return &updater.Release{}, nil +} +func (p *updateProviderFake) Download(context.Context, *updater.Release, io.Writer, func(int64, int64)) error { + p.downloads++ + return nil +} + +func TestUpdateProviderKeepsCheckedSourceForDownload(t *testing.T) { + official, mirror := &updateProviderFake{}, &updateProviderFake{} + preferMirror := true + provider := updateProvider{official: official, mirror: mirror, preferMirror: func(context.Context) bool { return preferMirror }} + + release, err := provider.Check(context.Background(), updater.CheckRequest{}) + if err != nil { + t.Fatal(err) + } + preferMirror = false + if err := provider.Download(context.Background(), release, &bytes.Buffer{}, func(int64, int64) {}); err != nil { + t.Fatal(err) + } + if mirror.checks != 1 || mirror.downloads != 1 || official.checks != 0 || official.downloads != 0 { + t.Fatalf("official checks/downloads = %d/%d, mirror = %d/%d", official.checks, official.downloads, mirror.checks, mirror.downloads) + } +} + type restartBackend struct { binding.UpdateBackend err error