feat(update): 添加在线更新和定价数据获取的代理支持
针对国内服务器访问 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 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
||||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
|
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
|
||||||
updateCache := repository.NewUpdateCache(redisClient)
|
updateCache := repository.NewUpdateCache(redisClient)
|
||||||
gitHubReleaseClient := repository.NewGitHubReleaseClient()
|
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||||
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
||||||
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
||||||
systemHandler := handler.ProvideSystemHandler(updateService)
|
systemHandler := handler.ProvideSystemHandler(updateService)
|
||||||
@@ -125,7 +125,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
||||||
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
|
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
|
||||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler)
|
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)
|
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ type Config struct {
|
|||||||
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
||||||
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
||||||
Gemini GeminiConfig `mapstructure:"gemini"`
|
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 {
|
type GeminiConfig struct {
|
||||||
@@ -558,6 +567,10 @@ func setDefaults() {
|
|||||||
viper.SetDefault("gemini.oauth.client_secret", "")
|
viper.SetDefault("gemini.oauth.client_secret", "")
|
||||||
viper.SetDefault("gemini.oauth.scopes", "")
|
viper.SetDefault("gemini.oauth.scopes", "")
|
||||||
viper.SetDefault("gemini.quota.policy", "")
|
viper.SetDefault("gemini.quota.policy", "")
|
||||||
|
|
||||||
|
// Update - 在线更新配置
|
||||||
|
// 代理地址为空表示直连 GitHub(适用于海外服务器)
|
||||||
|
viper.SetDefault("update.proxy_url", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
|
|||||||
@@ -15,22 +15,32 @@ import (
|
|||||||
|
|
||||||
type githubReleaseClient struct {
|
type githubReleaseClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
allowPrivateHosts bool
|
downloadHTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGitHubReleaseClient() service.GitHubReleaseClient {
|
// NewGitHubReleaseClient 创建 GitHub Release 客户端
|
||||||
allowPrivate := false
|
// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议
|
||||||
|
func NewGitHubReleaseClient(proxyURL string) service.GitHubReleaseClient {
|
||||||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
ValidateResolvedIP: true,
|
ProxyURL: proxyURL,
|
||||||
AllowPrivateHosts: allowPrivate,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
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{
|
return &githubReleaseClient{
|
||||||
httpClient: sharedClient,
|
httpClient: sharedClient,
|
||||||
allowPrivateHosts: allowPrivate,
|
downloadHTTPClient: downloadClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,15 +78,8 @@ func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
// 使用预配置的下载客户端(已包含代理配置)
|
||||||
Timeout: 10 * time.Minute,
|
resp, err := c.downloadHTTPClient.Do(req)
|
||||||
ValidateResolvedIP: true,
|
|
||||||
AllowPrivateHosts: c.allowPrivateHosts,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
|
||||||
}
|
|
||||||
resp, err := downloadClient.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
func newTestGitHubReleaseClient() *githubReleaseClient {
|
func newTestGitHubReleaseClient() *githubReleaseClient {
|
||||||
return &githubReleaseClient{
|
return &githubReleaseClient{
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
release, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||||
@@ -254,7 +254,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Non200() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||||
@@ -272,7 +272,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_InvalidJSON() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||||
@@ -288,7 +288,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_ContextCancel() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
@@ -17,17 +16,12 @@ type pricingRemoteClient struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPricingRemoteClient(cfg *config.Config) service.PricingRemoteClient {
|
// NewPricingRemoteClient 创建定价数据远程客户端
|
||||||
allowPrivate := false
|
// proxyURL 为空时直连,支持 http/https/socks5/socks5h 协议
|
||||||
validateResolvedIP := true
|
func NewPricingRemoteClient(proxyURL string) service.PricingRemoteClient {
|
||||||
if cfg != nil {
|
|
||||||
allowPrivate = cfg.Security.URLAllowlist.AllowPrivateHosts
|
|
||||||
validateResolvedIP = cfg.Security.URLAllowlist.Enabled
|
|
||||||
}
|
|
||||||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
ValidateResolvedIP: validateResolvedIP,
|
ProxyURL: proxyURL,
|
||||||
AllowPrivateHosts: allowPrivate,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
@@ -20,13 +19,7 @@ type PricingServiceSuite struct {
|
|||||||
|
|
||||||
func (s *PricingServiceSuite) SetupTest() {
|
func (s *PricingServiceSuite) SetupTest() {
|
||||||
s.ctx = context.Background()
|
s.ctx = context.Background()
|
||||||
client, ok := NewPricingRemoteClient(&config.Config{
|
client, ok := NewPricingRemoteClient("").(*pricingRemoteClient)
|
||||||
Security: config.SecurityConfig{
|
|
||||||
URLAllowlist: config.URLAllowlistConfig{
|
|
||||||
AllowPrivateHosts: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).(*pricingRemoteClient)
|
|
||||||
require.True(s.T(), ok, "type assertion failed")
|
require.True(s.T(), ok, "type assertion failed")
|
||||||
s.client = client
|
s.client = client
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ func ProvideConcurrencyCache(rdb *redis.Client, cfg *config.Config) service.Conc
|
|||||||
return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds)
|
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
|
// ProviderSet is the Wire provider set for all repositories
|
||||||
var ProviderSet = wire.NewSet(
|
var ProviderSet = wire.NewSet(
|
||||||
NewUserRepository,
|
NewUserRepository,
|
||||||
@@ -53,8 +65,8 @@ var ProviderSet = wire.NewSet(
|
|||||||
|
|
||||||
// HTTP service ports (DI Strategy A: return interface directly)
|
// HTTP service ports (DI Strategy A: return interface directly)
|
||||||
NewTurnstileVerifier,
|
NewTurnstileVerifier,
|
||||||
NewPricingRemoteClient,
|
ProvidePricingRemoteClient,
|
||||||
NewGitHubReleaseClient,
|
ProvideGitHubReleaseClient,
|
||||||
NewProxyExitInfoProber,
|
NewProxyExitInfoProber,
|
||||||
NewClaudeUsageFetcher,
|
NewClaudeUsageFetcher,
|
||||||
NewClaudeOAuthClient,
|
NewClaudeOAuthClient,
|
||||||
|
|||||||
@@ -123,3 +123,17 @@ GEMINI_OAUTH_SCOPES=
|
|||||||
# Example:
|
# 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={"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=
|
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=
|
||||||
|
|||||||
@@ -388,3 +388,18 @@ gemini:
|
|||||||
# Cooldown time (minutes) after hitting quota
|
# Cooldown time (minutes) after hitting quota
|
||||||
# 达到配额后的冷却时间(分钟)
|
# 达到配额后的冷却时间(分钟)
|
||||||
cooldown_minutes: 5
|
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: ""
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ services:
|
|||||||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||||||
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
|
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
|
||||||
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
- 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:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Reference in New Issue
Block a user