From 38d875b06feb317f35d6d8c5a5a42b7759ac3edb Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 6 Jan 2026 15:55:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(update):=20=E6=B7=BB=E5=8A=A0=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E6=9B=B4=E6=96=B0=E5=92=8C=E5=AE=9A=E4=BB=B7=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=8E=B7=E5=8F=96=E7=9A=84=E4=BB=A3=E7=90=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对国内服务器访问 GitHub 困难的问题,为在线更新和定价数据获取功能添加代理支持。 主要变更: - 新增 update.proxy_url 配置项,支持 http/https/socks5/socks5h 协议 - 修改 GitHubReleaseClient 和 PricingRemoteClient 支持代理配置 - 更新 Wire 依赖注入,通过 Provider 函数传递配置 - 更新 Docker 配置文件,支持通过 UPDATE_PROXY_URL 环境变量设置代理 配置示例: update: proxy_url: "http://127.0.0.1:7890" Docker 环境变量: UPDATE_PROXY_URL=http://host.docker.internal:7890 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/cmd/server/wire_gen.go | 4 +- backend/internal/config/config.go | 13 +++++++ .../repository/github_release_service.go | 39 ++++++++++--------- .../repository/github_release_service_test.go | 12 +++--- .../internal/repository/pricing_service.go | 16 +++----- .../repository/pricing_service_test.go | 9 +---- backend/internal/repository/wire.go | 16 +++++++- deploy/.env.example | 14 +++++++ deploy/config.example.yaml | 15 +++++++ deploy/docker-compose.yml | 7 ++++ 10 files changed, 98 insertions(+), 47 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 768254f9..5c17e822 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -114,7 +114,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { adminRedeemHandler := admin.NewRedeemHandler(adminService) settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService) updateCache := repository.NewUpdateCache(redisClient) - gitHubReleaseClient := repository.NewGitHubReleaseClient() + gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig) serviceBuildInfo := provideServiceBuildInfo(buildInfo) updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo) systemHandler := handler.ProvideSystemHandler(updateService) @@ -125,7 +125,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository) userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler) - pricingRemoteClient := repository.NewPricingRemoteClient(configConfig) + pricingRemoteClient := repository.ProvidePricingRemoteClient(configConfig) pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient) if err != nil { return nil, err diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index e49c188b..c1e15290 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -52,6 +52,15 @@ type Config struct { RunMode string `mapstructure:"run_mode" yaml:"run_mode"` Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC" Gemini GeminiConfig `mapstructure:"gemini"` + Update UpdateConfig `mapstructure:"update"` +} + +// UpdateConfig 在线更新相关配置 +type UpdateConfig struct { + // ProxyURL 用于访问 GitHub 的代理地址 + // 支持 http/https/socks5/socks5h 协议 + // 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080" + ProxyURL string `mapstructure:"proxy_url"` } type GeminiConfig struct { @@ -558,6 +567,10 @@ func setDefaults() { viper.SetDefault("gemini.oauth.client_secret", "") viper.SetDefault("gemini.oauth.scopes", "") viper.SetDefault("gemini.quota.policy", "") + + // Update - 在线更新配置 + // 代理地址为空表示直连 GitHub(适用于海外服务器) + viper.SetDefault("update.proxy_url", "") } func (c *Config) Validate() error { diff --git a/backend/internal/repository/github_release_service.go b/backend/internal/repository/github_release_service.go index dd53c091..77839626 100644 --- a/backend/internal/repository/github_release_service.go +++ b/backend/internal/repository/github_release_service.go @@ -14,23 +14,33 @@ import ( ) type githubReleaseClient struct { - httpClient *http.Client - allowPrivateHosts bool + httpClient *http.Client + downloadHTTPClient *http.Client } -func NewGitHubReleaseClient() service.GitHubReleaseClient { - allowPrivate := false +// NewGitHubReleaseClient 创建 GitHub Release 客户端 +// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议 +func NewGitHubReleaseClient(proxyURL string) service.GitHubReleaseClient { sharedClient, err := httpclient.GetClient(httpclient.Options{ - Timeout: 30 * time.Second, - ValidateResolvedIP: true, - AllowPrivateHosts: allowPrivate, + Timeout: 30 * time.Second, + ProxyURL: proxyURL, }) if err != nil { sharedClient = &http.Client{Timeout: 30 * time.Second} } + + // 下载客户端需要更长的超时时间 + downloadClient, err := httpclient.GetClient(httpclient.Options{ + Timeout: 10 * time.Minute, + ProxyURL: proxyURL, + }) + if err != nil { + downloadClient = &http.Client{Timeout: 10 * time.Minute} + } + return &githubReleaseClient{ - httpClient: sharedClient, - allowPrivateHosts: allowPrivate, + httpClient: sharedClient, + downloadHTTPClient: downloadClient, } } @@ -68,15 +78,8 @@ func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string return err } - downloadClient, err := httpclient.GetClient(httpclient.Options{ - Timeout: 10 * time.Minute, - ValidateResolvedIP: true, - AllowPrivateHosts: c.allowPrivateHosts, - }) - if err != nil { - downloadClient = &http.Client{Timeout: 10 * time.Minute} - } - resp, err := downloadClient.Do(req) + // 使用预配置的下载客户端(已包含代理配置) + resp, err := c.downloadHTTPClient.Do(req) if err != nil { return err } diff --git a/backend/internal/repository/github_release_service_test.go b/backend/internal/repository/github_release_service_test.go index 4eebe81d..d375a193 100644 --- a/backend/internal/repository/github_release_service_test.go +++ b/backend/internal/repository/github_release_service_test.go @@ -39,8 +39,8 @@ func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) { func newTestGitHubReleaseClient() *githubReleaseClient { return &githubReleaseClient{ - httpClient: &http.Client{}, - allowPrivateHosts: true, + httpClient: &http.Client{}, + downloadHTTPClient: &http.Client{}, } } @@ -234,7 +234,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() { httpClient: &http.Client{ Transport: &testTransport{testServerURL: s.srv.URL}, }, - allowPrivateHosts: true, + downloadHTTPClient: &http.Client{}, } release, err := s.client.FetchLatestRelease(context.Background(), "test/repo") @@ -254,7 +254,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Non200() { httpClient: &http.Client{ Transport: &testTransport{testServerURL: s.srv.URL}, }, - allowPrivateHosts: true, + downloadHTTPClient: &http.Client{}, } _, err := s.client.FetchLatestRelease(context.Background(), "test/repo") @@ -272,7 +272,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_InvalidJSON() { httpClient: &http.Client{ Transport: &testTransport{testServerURL: s.srv.URL}, }, - allowPrivateHosts: true, + downloadHTTPClient: &http.Client{}, } _, err := s.client.FetchLatestRelease(context.Background(), "test/repo") @@ -288,7 +288,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_ContextCancel() { httpClient: &http.Client{ Transport: &testTransport{testServerURL: s.srv.URL}, }, - allowPrivateHosts: true, + downloadHTTPClient: &http.Client{}, } ctx, cancel := context.WithCancel(context.Background()) diff --git a/backend/internal/repository/pricing_service.go b/backend/internal/repository/pricing_service.go index 791c89c6..07d796b8 100644 --- a/backend/internal/repository/pricing_service.go +++ b/backend/internal/repository/pricing_service.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/httpclient" "github.com/Wei-Shaw/sub2api/internal/service" ) @@ -17,17 +16,12 @@ type pricingRemoteClient struct { httpClient *http.Client } -func NewPricingRemoteClient(cfg *config.Config) service.PricingRemoteClient { - allowPrivate := false - validateResolvedIP := true - if cfg != nil { - allowPrivate = cfg.Security.URLAllowlist.AllowPrivateHosts - validateResolvedIP = cfg.Security.URLAllowlist.Enabled - } +// NewPricingRemoteClient 创建定价数据远程客户端 +// proxyURL 为空时直连,支持 http/https/socks5/socks5h 协议 +func NewPricingRemoteClient(proxyURL string) service.PricingRemoteClient { sharedClient, err := httpclient.GetClient(httpclient.Options{ - Timeout: 30 * time.Second, - ValidateResolvedIP: validateResolvedIP, - AllowPrivateHosts: allowPrivate, + Timeout: 30 * time.Second, + ProxyURL: proxyURL, }) if err != nil { sharedClient = &http.Client{Timeout: 30 * time.Second} diff --git a/backend/internal/repository/pricing_service_test.go b/backend/internal/repository/pricing_service_test.go index 6745ac58..6ea11211 100644 --- a/backend/internal/repository/pricing_service_test.go +++ b/backend/internal/repository/pricing_service_test.go @@ -6,7 +6,6 @@ import ( "net/http/httptest" "testing" - "github.com/Wei-Shaw/sub2api/internal/config" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -20,13 +19,7 @@ type PricingServiceSuite struct { func (s *PricingServiceSuite) SetupTest() { s.ctx = context.Background() - client, ok := NewPricingRemoteClient(&config.Config{ - Security: config.SecurityConfig{ - URLAllowlist: config.URLAllowlistConfig{ - AllowPrivateHosts: true, - }, - }, - }).(*pricingRemoteClient) + client, ok := NewPricingRemoteClient("").(*pricingRemoteClient) require.True(s.T(), ok, "type assertion failed") s.client = client } diff --git a/backend/internal/repository/wire.go b/backend/internal/repository/wire.go index f7574563..ba09f85e 100644 --- a/backend/internal/repository/wire.go +++ b/backend/internal/repository/wire.go @@ -25,6 +25,18 @@ func ProvideConcurrencyCache(rdb *redis.Client, cfg *config.Config) service.Conc return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds) } +// ProvideGitHubReleaseClient 创建 GitHub Release 客户端 +// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub +func ProvideGitHubReleaseClient(cfg *config.Config) service.GitHubReleaseClient { + return NewGitHubReleaseClient(cfg.Update.ProxyURL) +} + +// ProvidePricingRemoteClient 创建定价数据远程客户端 +// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub 上的定价数据 +func ProvidePricingRemoteClient(cfg *config.Config) service.PricingRemoteClient { + return NewPricingRemoteClient(cfg.Update.ProxyURL) +} + // ProviderSet is the Wire provider set for all repositories var ProviderSet = wire.NewSet( NewUserRepository, @@ -53,8 +65,8 @@ var ProviderSet = wire.NewSet( // HTTP service ports (DI Strategy A: return interface directly) NewTurnstileVerifier, - NewPricingRemoteClient, - NewGitHubReleaseClient, + ProvidePricingRemoteClient, + ProvideGitHubReleaseClient, NewProxyExitInfoProber, NewClaudeUsageFetcher, NewClaudeOAuthClient, diff --git a/deploy/.env.example b/deploy/.env.example index 93d97667..bd8abc5c 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -123,3 +123,17 @@ GEMINI_OAUTH_SCOPES= # Example: # GEMINI_QUOTA_POLICY={"tiers":{"LEGACY":{"pro_rpd":50,"flash_rpd":1500,"cooldown_minutes":30},"PRO":{"pro_rpd":1500,"flash_rpd":4000,"cooldown_minutes":5},"ULTRA":{"pro_rpd":2000,"flash_rpd":0,"cooldown_minutes":5}}} GEMINI_QUOTA_POLICY= + +# ----------------------------------------------------------------------------- +# Update Configuration (在线更新配置) +# ----------------------------------------------------------------------------- +# Proxy URL for accessing GitHub (used for online updates and pricing data) +# 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) +# Supports: http, https, socks5, socks5h +# Examples: +# HTTP proxy: http://127.0.0.1:7890 +# SOCKS5 proxy: socks5://127.0.0.1:1080 +# With authentication: http://user:pass@proxy.example.com:8080 +# Leave empty for direct connection (recommended for overseas servers) +# 留空表示直连(适用于海外服务器) +UPDATE_PROXY_URL= diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index 84f5f578..49bf0afa 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -388,3 +388,18 @@ gemini: # Cooldown time (minutes) after hitting quota # 达到配额后的冷却时间(分钟) cooldown_minutes: 5 + +# ============================================================================= +# Update Configuration (在线更新配置) +# ============================================================================= +update: + # Proxy URL for accessing GitHub (used for online updates and pricing data) + # 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) + # Supports: http, https, socks5, socks5h + # Examples: + # - HTTP proxy: "http://127.0.0.1:7890" + # - SOCKS5 proxy: "socks5://127.0.0.1:1080" + # - With authentication: "http://user:pass@proxy.example.com:8080" + # Leave empty for direct connection (recommended for overseas servers) + # 留空表示直连(适用于海外服务器) + proxy_url: "" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index cc10ed63..6a370e9a 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -109,6 +109,13 @@ services: - SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false} # Upstream hosts whitelist (comma-separated, only used when enabled=true) - SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-} + + # ======================================================================= + # Update Configuration (在线更新配置) + # ======================================================================= + # Proxy for accessing GitHub (online updates + pricing data) + # Examples: http://host:port, socks5://host:port + - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-} depends_on: postgres: condition: service_healthy