mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
fix: keep launch proxy overrides temporary
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package backend
|
||||
|
||||
func (a *App) BrowserInstanceStart(profileId string) (*BrowserProfile, error) {
|
||||
return a.browserInstanceStartInternal(profileId, nil, nil, false, false, false)
|
||||
return a.browserInstanceStartInternal(profileId, nil, nil, false, false, false, "", "")
|
||||
}
|
||||
|
||||
func shouldPreferVisibleWindowForStartWithParams(startURLs []string) bool {
|
||||
@@ -10,17 +10,17 @@ func shouldPreferVisibleWindowForStartWithParams(startURLs []string) bool {
|
||||
|
||||
// BrowserInstanceStartDirect 仅本次启动走直连,不落库修改实例代理配置。
|
||||
func (a *App) BrowserInstanceStartDirect(profileId string) (*BrowserProfile, error) {
|
||||
return a.browserInstanceStartInternal(profileId, nil, nil, false, false, true)
|
||||
return a.browserInstanceStartInternal(profileId, nil, nil, false, false, true, "", "")
|
||||
}
|
||||
|
||||
// BrowserInstanceStartWithParams 通过额外参数启动实例(仅本次启动生效,不落库)
|
||||
func (a *App) BrowserInstanceStartWithParams(profileId string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool) (*BrowserProfile, error) {
|
||||
preferVisibleWindow := shouldPreferVisibleWindowForStartWithParams(startURLs)
|
||||
return a.browserInstanceStartInternal(profileId, extraLaunchArgs, startURLs, skipDefaultStartURLs, preferVisibleWindow, false)
|
||||
return a.browserInstanceStartInternal(profileId, extraLaunchArgs, startURLs, skipDefaultStartURLs, preferVisibleWindow, false, "", "")
|
||||
}
|
||||
|
||||
func (a *App) browserInstanceStartInternal(profileId string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool, preferVisibleWindow bool, forceDirectProxy bool) (*BrowserProfile, error) {
|
||||
input := newBrowserStartInput(profileId, extraLaunchArgs, startURLs, skipDefaultStartURLs, preferVisibleWindow, forceDirectProxy)
|
||||
func (a *App) browserInstanceStartInternal(profileId string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool, preferVisibleWindow bool, forceDirectProxy bool, proxyId string, proxyConfig string) (*BrowserProfile, error) {
|
||||
input := newBrowserStartInput(profileId, extraLaunchArgs, startURLs, skipDefaultStartURLs, preferVisibleWindow, forceDirectProxy, proxyId, proxyConfig)
|
||||
a.browserMgr.Mutex.Lock()
|
||||
defer a.browserMgr.Mutex.Unlock()
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ type browserStartInput struct {
|
||||
SkipDefaultStartURLs bool
|
||||
PreferVisibleWindow bool
|
||||
ForceDirectProxy bool
|
||||
TemporaryProxyID string
|
||||
TemporaryProxyConfig string
|
||||
}
|
||||
|
||||
type browserStartPlan struct {
|
||||
@@ -34,7 +36,7 @@ type browserStartPlan struct {
|
||||
totalReadyTimeout time.Duration
|
||||
}
|
||||
|
||||
func newBrowserStartInput(profileID string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool, preferVisibleWindow bool, forceDirectProxy bool) browserStartInput {
|
||||
func newBrowserStartInput(profileID string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool, preferVisibleWindow bool, forceDirectProxy bool, proxyID string, proxyConfig string) browserStartInput {
|
||||
normalizedExtraLaunchArgs := normalizeNonEmptyStrings(extraLaunchArgs)
|
||||
if preferVisibleWindow {
|
||||
normalizedExtraLaunchArgs = ensureNewWindowLaunchArg(normalizedExtraLaunchArgs)
|
||||
@@ -47,9 +49,15 @@ func newBrowserStartInput(profileID string, extraLaunchArgs []string, startURLs
|
||||
SkipDefaultStartURLs: skipDefaultStartURLs,
|
||||
PreferVisibleWindow: preferVisibleWindow,
|
||||
ForceDirectProxy: forceDirectProxy,
|
||||
TemporaryProxyID: strings.TrimSpace(proxyID),
|
||||
TemporaryProxyConfig: strings.TrimSpace(proxyConfig),
|
||||
}
|
||||
}
|
||||
|
||||
func (input browserStartInput) hasTemporaryProxy() bool {
|
||||
return strings.TrimSpace(input.TemporaryProxyID) != "" || strings.TrimSpace(input.TemporaryProxyConfig) != ""
|
||||
}
|
||||
|
||||
func (plan *browserStartPlan) releaseBridgeIfNeeded(a *App) {
|
||||
if plan == nil || a == nil {
|
||||
return
|
||||
|
||||
@@ -7,15 +7,42 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const temporaryDirectProxyID = "__direct__"
|
||||
|
||||
func (a *App) resolveBrowserStartProxy(input browserStartInput, profile *BrowserProfile) (string, string, bool, error) {
|
||||
log := logger.New("Browser")
|
||||
proxies := a.getLatestProxies()
|
||||
profileID := input.ProfileID
|
||||
|
||||
if input.ForceDirectProxy {
|
||||
log.Warn("按请求直连启动实例",
|
||||
logger.F("profile_id", profileID),
|
||||
logger.F("proxy_id", profile.ProxyId),
|
||||
)
|
||||
return "direct://", "", false, nil
|
||||
}
|
||||
|
||||
resolvedProxyID := strings.TrimSpace(profile.ProxyId)
|
||||
resolvedProxyConfig := strings.TrimSpace(profile.ProxyConfig)
|
||||
if profile.ProxyId != "" {
|
||||
usingTemporaryProxy := input.hasTemporaryProxy()
|
||||
if usingTemporaryProxy {
|
||||
var err error
|
||||
resolvedProxyID, resolvedProxyConfig, err = resolveTemporaryBrowserStartProxy(input.TemporaryProxyID, input.TemporaryProxyConfig, proxies)
|
||||
if err != nil {
|
||||
startErr := fmt.Errorf("实例启动失败:%s", err.Error())
|
||||
profile.LastError = startErr.Error()
|
||||
log.Error("一次性代理配置无效",
|
||||
logger.F("profile_id", profileID),
|
||||
logger.F("temporary_proxy_id", input.TemporaryProxyID),
|
||||
logger.F("error", err.Error()),
|
||||
logger.F("reason", startErr.Error()),
|
||||
)
|
||||
return "", "", false, startErr
|
||||
}
|
||||
} else if resolvedProxyID != "" {
|
||||
for _, item := range proxies {
|
||||
if strings.EqualFold(item.ProxyId, profile.ProxyId) {
|
||||
if strings.EqualFold(item.ProxyId, resolvedProxyID) {
|
||||
resolvedProxyID = strings.TrimSpace(item.ProxyId)
|
||||
resolvedProxyConfig = strings.TrimSpace(item.ProxyConfig)
|
||||
break
|
||||
}
|
||||
@@ -26,21 +53,17 @@ func (a *App) resolveBrowserStartProxy(input browserStartInput, profile *Browser
|
||||
logger.F("profile_id", profileID),
|
||||
logger.F("proxy_id", profile.ProxyId),
|
||||
logger.F("profile_proxy_config", profile.ProxyConfig),
|
||||
logger.F("temporary_proxy", usingTemporaryProxy),
|
||||
logger.F("temporary_proxy_id", input.TemporaryProxyID),
|
||||
logger.F("temporary_proxy_config", input.TemporaryProxyConfig),
|
||||
logger.F("resolved_proxy_config", resolvedProxyConfig),
|
||||
)
|
||||
if input.ForceDirectProxy {
|
||||
log.Warn("按请求直连启动实例",
|
||||
logger.F("profile_id", profileID),
|
||||
logger.F("proxy_id", profile.ProxyId),
|
||||
)
|
||||
return "direct://", "", false, nil
|
||||
}
|
||||
if supported, errorMsg := proxy.ValidateProxyConfig(resolvedProxyConfig, proxies, profile.ProxyId); !supported {
|
||||
if supported, errorMsg := proxy.ValidateProxyConfig(resolvedProxyConfig, proxies, resolvedProxyID); !supported {
|
||||
startErr := fmt.Errorf("实例启动失败:%s", errorMsg)
|
||||
profile.LastError = startErr.Error()
|
||||
log.Error("代理配置无效",
|
||||
logger.F("profile_id", profileID),
|
||||
logger.F("proxy_id", profile.ProxyId),
|
||||
logger.F("proxy_id", resolvedProxyID),
|
||||
logger.F("error", errorMsg),
|
||||
logger.F("reason", startErr.Error()),
|
||||
)
|
||||
@@ -48,7 +71,7 @@ func (a *App) resolveBrowserStartProxy(input browserStartInput, profile *Browser
|
||||
}
|
||||
|
||||
if proxy.IsSingBoxProtocol(resolvedProxyConfig) {
|
||||
socksURL, bridgeErr := a.singboxMgr.EnsureBridge(resolvedProxyConfig, proxies, profile.ProxyId)
|
||||
socksURL, bridgeErr := a.singboxMgr.EnsureBridge(resolvedProxyConfig, proxies, resolvedProxyID)
|
||||
if bridgeErr != nil {
|
||||
startErr := fmt.Errorf("实例启动失败:代理桥接启动失败(sing-box)。原因:%v。请检查代理节点配置、sing-box 可执行文件是否存在,以及本地端口是否被占用。", bridgeErr)
|
||||
log.Error("代理桥接失败(sing-box)",
|
||||
@@ -62,8 +85,8 @@ func (a *App) resolveBrowserStartProxy(input browserStartInput, profile *Browser
|
||||
return socksURL, "", false, nil
|
||||
}
|
||||
|
||||
if proxy.RequiresBridge(resolvedProxyConfig, proxies, profile.ProxyId) || proxy.RequiresLocalProxyBridgeForBrowser(resolvedProxyConfig) {
|
||||
socksURL, bridgeKey, bridgeErr := a.xrayMgr.AcquireBridge(resolvedProxyConfig, proxies, profile.ProxyId)
|
||||
if proxy.RequiresBridge(resolvedProxyConfig, proxies, resolvedProxyID) || proxy.RequiresLocalProxyBridgeForBrowser(resolvedProxyConfig) {
|
||||
socksURL, bridgeKey, bridgeErr := a.xrayMgr.AcquireBridge(resolvedProxyConfig, proxies, resolvedProxyID)
|
||||
if bridgeErr != nil {
|
||||
startErr := fmt.Errorf("实例启动失败:代理桥接启动失败(xray)。原因:%v。请检查代理节点配置、xray 可执行文件是否存在,以及本地端口是否被占用。", bridgeErr)
|
||||
log.Error("代理桥接失败(xray)",
|
||||
@@ -79,3 +102,24 @@ func (a *App) resolveBrowserStartProxy(input browserStartInput, profile *Browser
|
||||
|
||||
return resolvedProxyConfig, "", false, nil
|
||||
}
|
||||
|
||||
func resolveTemporaryBrowserStartProxy(proxyID string, proxyConfig string, proxies []BrowserProxy) (string, string, error) {
|
||||
proxyID = strings.TrimSpace(proxyID)
|
||||
proxyConfig = strings.TrimSpace(proxyConfig)
|
||||
if proxyID == "" {
|
||||
return "", proxyConfig, nil
|
||||
}
|
||||
|
||||
for _, item := range proxies {
|
||||
if strings.EqualFold(item.ProxyId, proxyID) {
|
||||
return strings.TrimSpace(item.ProxyId), strings.TrimSpace(item.ProxyConfig), nil
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(proxyID, temporaryDirectProxyID) {
|
||||
return temporaryDirectProxyID, "direct://", nil
|
||||
}
|
||||
if proxyConfig != "" {
|
||||
return "", proxyConfig, nil
|
||||
}
|
||||
return "", "", fmt.Errorf("代理ID不存在(proxy id not found: %s),且未提供 proxyConfig", proxyID)
|
||||
}
|
||||
|
||||
@@ -461,6 +461,55 @@ func TestSanitizeManagedLaunchArgsKeepsUnmanagedFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBrowserStartProxyUsesTemporaryProxyWithoutMutatingProfile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Browser.Proxies = []config.BrowserProxy{
|
||||
{ProxyId: "stored-proxy", ProxyName: "Stored", ProxyConfig: "http://127.0.0.1:18080"},
|
||||
{ProxyId: "runtime-proxy", ProxyName: "Runtime", ProxyConfig: "http://127.0.0.1:28080"},
|
||||
}
|
||||
app := NewApp("")
|
||||
app.config = cfg
|
||||
app.browserMgr = browser.NewManager(cfg, t.TempDir())
|
||||
profile := &BrowserProfile{
|
||||
ProfileId: "profile-temporary-proxy",
|
||||
ProfileName: "Temporary Proxy",
|
||||
ProxyId: "stored-proxy",
|
||||
ProxyConfig: "http://127.0.0.1:18080",
|
||||
}
|
||||
input := newBrowserStartInput(profile.ProfileId, nil, nil, false, false, false, "runtime-proxy", "")
|
||||
|
||||
effectiveProxy, bridgeKey, releaseBridge, err := app.resolveBrowserStartProxy(input, profile)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveBrowserStartProxy returned error: %v", err)
|
||||
}
|
||||
if effectiveProxy != "http://127.0.0.1:28080" {
|
||||
t.Fatalf("expected temporary proxy, got %q", effectiveProxy)
|
||||
}
|
||||
if bridgeKey != "" || releaseBridge {
|
||||
t.Fatalf("plain HTTP proxy should not acquire bridge: key=%q release=%v", bridgeKey, releaseBridge)
|
||||
}
|
||||
if profile.ProxyId != "stored-proxy" || profile.ProxyConfig != "http://127.0.0.1:18080" {
|
||||
t.Fatalf("temporary proxy should not mutate profile: %+v", profile)
|
||||
}
|
||||
|
||||
fallbackInput := newBrowserStartInput(profile.ProfileId, nil, nil, false, false, false, "missing-proxy", "http://127.0.0.1:38080")
|
||||
effectiveProxy, bridgeKey, releaseBridge, err = app.resolveBrowserStartProxy(fallbackInput, profile)
|
||||
if err != nil {
|
||||
t.Fatalf("fallback temporary proxy returned error: %v", err)
|
||||
}
|
||||
if effectiveProxy != "http://127.0.0.1:38080" {
|
||||
t.Fatalf("expected fallback temporary proxy config, got %q", effectiveProxy)
|
||||
}
|
||||
if bridgeKey != "" || releaseBridge {
|
||||
t.Fatalf("fallback HTTP proxy should not acquire bridge: key=%q release=%v", bridgeKey, releaseBridge)
|
||||
}
|
||||
if profile.ProxyId != "stored-proxy" || profile.ProxyConfig != "http://127.0.0.1:18080" {
|
||||
t.Fatalf("fallback temporary proxy should not mutate profile: %+v", profile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendLaunchTargetsUsesConfiguredDefaultStartURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ func (a *App) StartInstance(profileId string) (*browser.Profile, error) {
|
||||
|
||||
// StartInstanceWithParams 实现 launchcode.BrowserStarterWithParams 接口
|
||||
func (a *App) StartInstanceWithParams(profileId string, params launchcode.LaunchRequestParams) (*browser.Profile, error) {
|
||||
return a.BrowserInstanceStartWithParams(profileId, params.LaunchArgs, params.StartURLs, params.SkipDefaultStartURLs)
|
||||
preferVisibleWindow := shouldPreferVisibleWindowForStartWithParams(params.StartURLs)
|
||||
return a.browserInstanceStartInternal(profileId, params.LaunchArgs, params.StartURLs, params.SkipDefaultStartURLs, preferVisibleWindow, false, params.ProxyId, params.ProxyConfig)
|
||||
}
|
||||
|
||||
// StatusInstance 实现 launchcode.BrowserStatusProvider 接口
|
||||
|
||||
@@ -60,11 +60,7 @@ func (s *LaunchServer) maybeAutoLaunchProfile(profile *browser.Profile, req Prof
|
||||
|
||||
params := LaunchRequestParams{}
|
||||
if req.Start != nil {
|
||||
params = LaunchRequestParams{
|
||||
LaunchArgs: normalizeStringSlice(req.Start.LaunchArgs),
|
||||
StartURLs: normalizeStringSlice(req.Start.StartURLs),
|
||||
SkipDefaultStartURLs: req.Start.SkipDefaultStartURLs,
|
||||
}
|
||||
params = normalizeLaunchRequestParams(*req.Start)
|
||||
}
|
||||
|
||||
launchedProfile, err := s.launchProfile(profile.ProfileId, params)
|
||||
|
||||
@@ -85,8 +85,7 @@ func (s *LaunchServer) handleRuntimeSession(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
req.LaunchArgs = normalizeStringSlice(req.LaunchArgs)
|
||||
req.StartURLs = normalizeStringSlice(req.StartURLs)
|
||||
req.LaunchRequestParams = normalizeLaunchRequestParams(req.LaunchRequestParams)
|
||||
profile, launchCode, status, errMsg := s.launchBySelector(selector, req.LaunchRequestParams)
|
||||
if errMsg != "" {
|
||||
writeJSON(w, status, map[string]interface{}{
|
||||
|
||||
@@ -30,6 +30,8 @@ type LaunchRequestParams struct {
|
||||
LaunchArgs []string `json:"launchArgs"`
|
||||
StartURLs []string `json:"startUrls"`
|
||||
SkipDefaultStartURLs bool `json:"skipDefaultStartUrls"`
|
||||
ProxyId string `json:"proxyId"`
|
||||
ProxyConfig string `json:"proxyConfig"`
|
||||
}
|
||||
|
||||
// LaunchRequest POST /api/launch 的请求体
|
||||
|
||||
@@ -90,8 +90,7 @@ func (s *LaunchServer) handleLaunchWithBody(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
req.LaunchArgs = normalizeStringSlice(req.LaunchArgs)
|
||||
req.StartURLs = normalizeStringSlice(req.StartURLs)
|
||||
req.LaunchRequestParams = normalizeLaunchRequestParams(req.LaunchRequestParams)
|
||||
if selector.MatchMode == launchMatchModeAll {
|
||||
profiles, status, errMsg := s.launchAllBySelector(selector, req.LaunchRequestParams)
|
||||
if errMsg != "" {
|
||||
|
||||
@@ -75,6 +75,14 @@ func normalizeStringSlice(items []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeLaunchRequestParams(params LaunchRequestParams) LaunchRequestParams {
|
||||
params.LaunchArgs = normalizeStringSlice(params.LaunchArgs)
|
||||
params.StartURLs = normalizeStringSlice(params.StartURLs)
|
||||
params.ProxyId = strings.TrimSpace(params.ProxyId)
|
||||
params.ProxyConfig = strings.TrimSpace(params.ProxyConfig)
|
||||
return params
|
||||
}
|
||||
|
||||
func remoteIP(remoteAddr string) string {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
|
||||
@@ -93,6 +93,53 @@ func TestLaunchWithParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchWithTemporaryProxyParams(t *testing.T) {
|
||||
svc := newInMemoryService()
|
||||
starter := newMockStarterWithParams()
|
||||
starter.addProfile(&browser.Profile{
|
||||
ProfileId: "profile-temporary-proxy",
|
||||
ProfileName: "temporary-proxy",
|
||||
ProxyId: "stored-proxy",
|
||||
ProxyConfig: "http://127.0.0.1:18080",
|
||||
Pid: 322,
|
||||
DebugPort: 9556,
|
||||
DebugReady: true,
|
||||
Running: true,
|
||||
LastStartAt: "2026-05-09T00:00:00Z",
|
||||
LastError: "",
|
||||
LaunchCode: "",
|
||||
RuntimeWarning: "",
|
||||
})
|
||||
|
||||
code, err := svc.EnsureCode("profile-temporary-proxy")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureCode 失败: %v", err)
|
||||
}
|
||||
|
||||
handler := buildTestHandler(svc, starter)
|
||||
body := map[string]interface{}{
|
||||
"code": code,
|
||||
"proxyConfig": " http://127.0.0.1:28080 ",
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/launch", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if starter.lastParams.ProxyConfig != "http://127.0.0.1:28080" {
|
||||
t.Fatalf("一次性 proxyConfig 未透传或未归一化: %+v", starter.lastParams)
|
||||
}
|
||||
profile := starter.profiles["profile-temporary-proxy"]
|
||||
if profile.ProxyConfig != "http://127.0.0.1:18080" || profile.ProxyId != "stored-proxy" {
|
||||
t.Fatalf("启动接口不应覆盖实例原代理: %+v", profile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchWithParamsUsingCodeAsKeywordFallback(t *testing.T) {
|
||||
svc := newInMemoryService()
|
||||
starter := newMockStarterWithParams()
|
||||
|
||||
@@ -488,6 +488,7 @@ export const STRUCTURED_API_ENDPOINT_DOCS: StructuredApiEndpointDoc[] = [
|
||||
notes: [
|
||||
'selector 为空且没有任何兼容顶层选择字段时返回 400。',
|
||||
'matchMode=all 只在这个接口可用。',
|
||||
'proxyId / proxyConfig 只影响本次启动,不覆盖实例原代理。',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -899,6 +899,8 @@ export namespace launchcode {
|
||||
launchArgs: string[];
|
||||
startUrls: string[];
|
||||
skipDefaultStartUrls: boolean;
|
||||
proxyId: string;
|
||||
proxyConfig: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new LaunchRequestParams(source);
|
||||
@@ -909,6 +911,8 @@ export namespace launchcode {
|
||||
this.launchArgs = source["launchArgs"];
|
||||
this.startUrls = source["startUrls"];
|
||||
this.skipDefaultStartUrls = source["skipDefaultStartUrls"];
|
||||
this.proxyId = source["proxyId"];
|
||||
this.proxyConfig = source["proxyConfig"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user