feat: implement custom proxy subscription management and enhance configuration

- Added support for importing Clash/V2ray subscriptions, including automatic format detection and integration with sing-box for protocol conversion.
- Introduced five proxy usage modes in the configuration, allowing flexible selection between mixed, custom-only, and free-only modes.
- Enhanced `.env.example` and `docker-compose.yml` to include new environment variables for custom proxy settings.
- Updated `CHANGELOG.md` to document new features and improvements related to subscription management.
- Improved WebUI for managing subscriptions and displaying proxy statistics.
- Implemented a background process for refreshing subscriptions and probing disabled proxies for reactivation.
This commit is contained in:
isboyjc
2026-04-04 22:25:54 +08:00
parent 2d118688a5
commit f03c3300b4
23 changed files with 5394 additions and 1488 deletions

View File

@@ -87,6 +87,15 @@ type Config struct {
SourceDisableThreshold int // 源禁用阈值默认5
SourceCooldownMinutes int // 源禁用冷却时间默认30
// ========== 自定义订阅代理配置 ==========
CustomProxyMode string // 代理使用模式mixed / custom_only / free_only默认 mixed
CustomPriority bool // 混用模式下订阅代理优先(默认 true
CustomFreePriority bool // 混用模式下免费代理优先(默认 false
CustomProbeInterval int // 禁用代理探测唤醒间隔(分钟,默认 10
CustomRefreshInterval int // 默认订阅刷新间隔(分钟,默认 60
SingBoxPath string // sing-box 二进制路径(默认 "sing-box"
SingBoxBasePort int // sing-box 本地端口起始(默认 20000
// ========== 兼容旧配置 ==========
MaxResponseMs int // 已废弃,使用 MaxLatencyMs 替代
MaxFailCount int // 代理失败次数阈值
@@ -154,6 +163,16 @@ func DefaultConfig() *Config {
}
}
// 读取订阅代理配置
customProxyMode := os.Getenv("CUSTOM_PROXY_MODE")
if customProxyMode == "" {
customProxyMode = "mixed"
}
singBoxPath := os.Getenv("SINGBOX_PATH")
if singBoxPath == "" {
singBoxPath = "sing-box"
}
return &Config{
// 基础服务配置
WebUIPort: ":7778",
@@ -208,6 +227,14 @@ func DefaultConfig() *Config {
SourceDisableThreshold: 5, // 失败5次禁用
SourceCooldownMinutes: 30, // 禁用30分钟
// 自定义订阅代理配置
CustomProxyMode: customProxyMode,
CustomPriority: true,
CustomProbeInterval: 10,
CustomRefreshInterval: 60,
SingBoxPath: singBoxPath,
SingBoxBasePort: 20000,
// 兼容旧配置
MaxResponseMs: 5000,
MaxFailCount: 3,
@@ -287,6 +314,29 @@ func Load() *Config {
if saved.AllowedCountries != nil {
cfg.AllowedCountries = saved.AllowedCountries
}
// 自定义订阅代理配置
if saved.CustomProxyMode != "" {
cfg.CustomProxyMode = saved.CustomProxyMode
}
if saved.CustomPriority != nil {
cfg.CustomPriority = *saved.CustomPriority
}
if saved.CustomFreePriority != nil {
cfg.CustomFreePriority = *saved.CustomFreePriority
}
if saved.CustomProbeInterval > 0 {
cfg.CustomProbeInterval = saved.CustomProbeInterval
}
if saved.CustomRefreshInterval > 0 {
cfg.CustomRefreshInterval = saved.CustomRefreshInterval
}
if saved.SingBoxPath != "" {
cfg.SingBoxPath = saved.SingBoxPath
}
if saved.SingBoxBasePort > 0 {
cfg.SingBoxBasePort = saved.SingBoxBasePort
}
}
}
cfgMu.Lock()
@@ -330,6 +380,15 @@ type savedConfig struct {
BlockedCountries []string `json:"blocked_countries,omitempty"`
AllowedCountries []string `json:"allowed_countries,omitempty"`
// 自定义订阅代理配置
CustomProxyMode string `json:"custom_proxy_mode,omitempty"`
CustomPriority *bool `json:"custom_priority,omitempty"`
CustomFreePriority *bool `json:"custom_free_priority,omitempty"`
CustomProbeInterval int `json:"custom_probe_interval,omitempty"`
CustomRefreshInterval int `json:"custom_refresh_interval,omitempty"`
SingBoxPath string `json:"singbox_path,omitempty"`
SingBoxBasePort int `json:"singbox_base_port,omitempty"`
// 兼容旧配置
FetchInterval int `json:"fetch_interval,omitempty"`
CheckInterval int `json:"check_interval,omitempty"`
@@ -341,23 +400,32 @@ func Save(cfg *Config) error {
*globalCfg = *cfg
cfgMu.Unlock()
customPriority := cfg.CustomPriority
customFreePriority := cfg.CustomFreePriority
data, err := json.MarshalIndent(savedConfig{
PoolMaxSize: cfg.PoolMaxSize,
PoolHTTPRatio: cfg.PoolHTTPRatio,
PoolMinPerProtocol: cfg.PoolMinPerProtocol,
MaxLatencyMs: cfg.MaxLatencyMs,
MaxLatencyEmergency: cfg.MaxLatencyEmergency,
MaxLatencyHealthy: cfg.MaxLatencyHealthy,
ValidateConcurrency: cfg.ValidateConcurrency,
ValidateTimeout: cfg.ValidateTimeout,
HealthCheckInterval: cfg.HealthCheckInterval,
HealthCheckBatchSize: cfg.HealthCheckBatchSize,
OptimizeInterval: cfg.OptimizeInterval,
ReplaceThreshold: cfg.ReplaceThreshold,
BlockedCountries: cfg.BlockedCountries,
AllowedCountries: cfg.AllowedCountries,
FetchInterval: cfg.FetchInterval,
CheckInterval: cfg.CheckInterval,
PoolMaxSize: cfg.PoolMaxSize,
PoolHTTPRatio: cfg.PoolHTTPRatio,
PoolMinPerProtocol: cfg.PoolMinPerProtocol,
MaxLatencyMs: cfg.MaxLatencyMs,
MaxLatencyEmergency: cfg.MaxLatencyEmergency,
MaxLatencyHealthy: cfg.MaxLatencyHealthy,
ValidateConcurrency: cfg.ValidateConcurrency,
ValidateTimeout: cfg.ValidateTimeout,
HealthCheckInterval: cfg.HealthCheckInterval,
HealthCheckBatchSize: cfg.HealthCheckBatchSize,
OptimizeInterval: cfg.OptimizeInterval,
ReplaceThreshold: cfg.ReplaceThreshold,
BlockedCountries: cfg.BlockedCountries,
AllowedCountries: cfg.AllowedCountries,
CustomProxyMode: cfg.CustomProxyMode,
CustomPriority: &customPriority,
CustomFreePriority: &customFreePriority,
CustomProbeInterval: cfg.CustomProbeInterval,
CustomRefreshInterval: cfg.CustomRefreshInterval,
SingBoxPath: cfg.SingBoxPath,
SingBoxBasePort: cfg.SingBoxBasePort,
FetchInterval: cfg.FetchInterval,
CheckInterval: cfg.CheckInterval,
}, "", " ")
if err != nil {
return err