feat: add reusable proxy configuration
This commit is contained in:
+94
-1
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"chub/internal/domain"
|
||||
@@ -110,6 +111,7 @@ func runWindow(logger *slog.Logger) {
|
||||
launcher := browser.NewOSLauncher()
|
||||
cdp := browser.NewCDPInspector()
|
||||
externalProfiles := browser.NewWindowsProfileInspector()
|
||||
proxies := newProxyDirectory()
|
||||
shell.OnStartInstance(instanceStarter{
|
||||
launcher: launcher,
|
||||
resolver: discoverer,
|
||||
@@ -117,6 +119,7 @@ func runWindow(logger *slog.Logger) {
|
||||
externalProfiles: externalProfiles,
|
||||
remoteDebug: cdp,
|
||||
portAllocator: cdp,
|
||||
proxyResolver: proxies,
|
||||
}.Start, window.Invalidate)
|
||||
shell.OnRefreshInstances(instanceStatusRefresher{
|
||||
managedProfiles: launcher,
|
||||
@@ -153,9 +156,11 @@ func runWindow(logger *slog.Logger) {
|
||||
if store, err := config.New(path); err == nil {
|
||||
if saved, err := store.Load(); err == nil {
|
||||
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, LogDir: saved.Settings.LogDir, RemoteDebugStartPort: saved.Settings.RemoteDebugStartPort, CloseOnExit: saved.Settings.CloseOnExit})
|
||||
proxies.Set(saved.Proxies)
|
||||
shell.SetProxies(proxyOptions(saved.Proxies))
|
||||
rows := make([]ui.InstanceRow, 0, len(saved.Instances))
|
||||
for _, item := range saved.Instances {
|
||||
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: browserLabel(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, TargetURL: item.Launch.TargetURL, Status: "已退出"})
|
||||
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: browserLabel(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, TargetURL: item.Launch.TargetURL, ProxyID: item.ProxyID, Status: "已退出"})
|
||||
}
|
||||
shell.SetInstances(rows)
|
||||
shell.OnSave(func(value ui.SettingsState) {
|
||||
@@ -166,6 +171,11 @@ func runWindow(logger *slog.Logger) {
|
||||
saved.Instances = mergeInstanceConfig(saved.Instances, rows)
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
shell.OnProxiesChanged(func(options []ui.ProxyOption) {
|
||||
saved.Proxies = mergeProxyConfig(saved.Proxies, options)
|
||||
proxies.Set(saved.Proxies)
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,6 +202,55 @@ func saveSettings(logger *slog.Logger, store *config.Store, value config.File) {
|
||||
}
|
||||
}
|
||||
|
||||
type proxyServerResolver interface {
|
||||
Resolve(string) (string, error)
|
||||
}
|
||||
|
||||
// proxyDirectory is the single in-memory authority used by UI launch requests.
|
||||
// It stores only endpoints already validated by config and never exposes secrets.
|
||||
type proxyDirectory struct {
|
||||
mu sync.RWMutex
|
||||
servers map[string]string
|
||||
}
|
||||
|
||||
func newProxyDirectory() *proxyDirectory {
|
||||
return &proxyDirectory{servers: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (d *proxyDirectory) Set(profiles []config.ProxyProfile) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
servers := make(map[string]string, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
servers[profile.ID] = profile.Server
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.servers = servers
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func (d *proxyDirectory) Resolve(id string) (string, error) {
|
||||
if d == nil || strings.TrimSpace(id) == "" {
|
||||
return "", errors.New("所选代理不可用")
|
||||
}
|
||||
d.mu.RLock()
|
||||
server, exists := d.servers[id]
|
||||
d.mu.RUnlock()
|
||||
if !exists {
|
||||
return "", errors.New("所选代理不可用")
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func proxyOptions(profiles []config.ProxyProfile) []ui.ProxyOption {
|
||||
options := make([]ui.ProxyOption, 0, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
options = append(options, ui.ProxyOption{ID: profile.ID, Name: profile.Name, Server: profile.Server})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
type executableResolver interface {
|
||||
Resolve(context.Context, domain.BrowserKind, string) (string, error)
|
||||
}
|
||||
@@ -211,6 +270,7 @@ type instanceStarter struct {
|
||||
externalProfiles browser.ExternalProfileInspector
|
||||
remoteDebug browser.RemoteDebugEndpointInspector
|
||||
portAllocator browser.RemoteDebugPortAllocator
|
||||
proxyResolver proxyServerResolver
|
||||
}
|
||||
|
||||
type instanceStatusRefresher struct {
|
||||
@@ -332,6 +392,16 @@ func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings
|
||||
return ui.InstanceStartOutcome{PID: use.PID, Source: use.Source}, &browser.ProfileOccupiedError{UserDataDir: row.UserDataDir, PID: use.PID}
|
||||
}
|
||||
}
|
||||
proxyServer := ""
|
||||
if row.ProxyID != "" {
|
||||
if s.proxyResolver == nil {
|
||||
return ui.InstanceStartOutcome{}, errors.New("所选代理不可用")
|
||||
}
|
||||
proxyServer, err = s.proxyResolver.Resolve(row.ProxyID)
|
||||
if err != nil {
|
||||
return ui.InstanceStartOutcome{}, err
|
||||
}
|
||||
}
|
||||
configuredExecutable := settings.ChromePath
|
||||
if kind == domain.BrowserEdge {
|
||||
configuredExecutable = settings.EdgePath
|
||||
@@ -350,6 +420,7 @@ func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings
|
||||
UserDataDir: row.UserDataDir,
|
||||
RemoteDebugPort: port,
|
||||
TargetURL: row.TargetURL,
|
||||
ProxyServer: proxyServer,
|
||||
})
|
||||
if err != nil {
|
||||
return ui.InstanceStartOutcome{}, err
|
||||
@@ -398,15 +469,37 @@ func mergeInstanceConfig(existing []config.Instance, rows []ui.InstanceRow) []co
|
||||
item.Launch.Kind = kind
|
||||
item.Launch.UserDataDir = row.UserDataDir
|
||||
item.Launch.TargetURL = row.TargetURL
|
||||
item.ProxyID = row.ProxyID
|
||||
if item.ProxyID != "" {
|
||||
item.Launch.ProxyServer = ""
|
||||
}
|
||||
item.UpdatedAt = time.Now()
|
||||
updated = append(updated, item)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func mergeProxyConfig(existing []config.ProxyProfile, options []ui.ProxyOption) []config.ProxyProfile {
|
||||
byID := make(map[string]config.ProxyProfile, len(existing))
|
||||
for _, profile := range existing {
|
||||
byID[profile.ID] = profile
|
||||
}
|
||||
updated := make([]config.ProxyProfile, 0, len(options))
|
||||
for _, option := range options {
|
||||
profile := byID[option.ID]
|
||||
profile.ID = option.ID
|
||||
profile.Name = option.Name
|
||||
profile.Server = option.Server
|
||||
profile.UpdatedAt = time.Now()
|
||||
updated = append(updated, profile)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func configSnapshot(value config.File) config.File {
|
||||
clone := value
|
||||
clone.Instances = append([]config.Instance(nil), value.Instances...)
|
||||
clone.Proxies = append([]config.ProxyProfile(nil), value.Proxies...)
|
||||
for i := range clone.Instances {
|
||||
clone.Instances[i].Launch.ExtraArgs = append([]string(nil), clone.Instances[i].Launch.ExtraArgs...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user