package backend import ( "ant-chrome/backend/internal/browser" "ant-chrome/backend/internal/logger" "fmt" "os" "path/filepath" "strings" "time" ) type browserStartInput struct { ProfileID string ExtraLaunchArgs []string StartURLs []string SkipDefaultStartURLs bool PreferVisibleWindow bool ForceDirectProxy bool TemporaryProxyID string TemporaryProxyConfig string } type browserStartPlan struct { profile *BrowserProfile chromeBinaryPath string userDataDir string args []string extensionDirs []string deferredStartTargets []string effectiveProxy string acquiredProxyBridge profileProxyBridgeRef releaseProxyBridge bool assignedDebugPort int startReadyTimeout time.Duration startStableWindow time.Duration maxStartAttempts int totalReadyTimeout time.Duration } var clearBrowserSessionRestoreData = browser.ClearSessionRestoreData func newBrowserStartInput(profileID string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool, preferVisibleWindow bool, forceDirectProxy bool, proxyID string, proxyConfig string) browserStartInput { normalizedExtraLaunchArgs := normalizeNonEmptyStrings(extraLaunchArgs) return browserStartInput{ ProfileID: profileID, ExtraLaunchArgs: normalizedExtraLaunchArgs, StartURLs: normalizeNonEmptyStrings(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 } if plan.releaseProxyBridge { a.releaseProxyBridgeRef(plan.acquiredProxyBridge) } } func (a *App) resolveBrowserStartProfile(input browserStartInput) (*BrowserProfile, bool, error) { log := logger.New("Browser") profile, exists := a.browserMgr.Profiles[input.ProfileID] if !exists { err := fmt.Errorf("实例启动失败:未找到实例配置(ID=%s)。请刷新列表后重试。", input.ProfileID) log.Error("实例不存在", logger.F("profile_id", input.ProfileID), logger.F("reason", err.Error())) return nil, false, err } a.ensureProfileLaunchCode(profile) if !profile.Running { return profile, false, nil } if !isBrowserProfileLive(profile, a.browserMgr.BrowserProcesses[input.ProfileID]) { log.Info("检测到实例运行状态已失效,准备重新启动", logger.F("profile_id", input.ProfileID), logger.F("pid", profile.Pid), logger.F("debug_port", profile.DebugPort), ) a.markProfileStoppedLocked(input.ProfileID, profile) return profile, false, nil } if len(normalizeNonEmptyStrings(input.StartURLs)) == 0 && len(normalizeNonEmptyStrings(input.ExtraLaunchArgs)) == 0 { if a.launchServer != nil && profile.DebugReady { a.launchServer.SetActiveProfile(profile) } a.emitBrowserInstanceStarted(profile, true) return profile, true, nil } if err := a.openBrowserTabForRunningProfile(profile, input.ExtraLaunchArgs, input.StartURLs); err != nil { startErr := fmt.Errorf("实例已在运行,但新标签打开失败:%w", err) log.Error("运行中实例新标签打开失败", logger.F("profile_id", input.ProfileID), logger.F("debug_port", profile.DebugPort), logger.F("error", err.Error()), logger.F("reason", startErr.Error()), ) profile.LastError = startErr.Error() return profile, true, startErr } if a.launchServer != nil && profile.DebugReady { a.launchServer.SetActiveProfile(profile) } a.emitBrowserInstanceStarted(profile, true) return profile, true, nil } func (a *App) prepareBrowserStartPlan(input browserStartInput, profile *BrowserProfile) (*browserStartPlan, error) { bookmarks := a.BookmarkList() sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, chromeBinaryPath, userDataDir, err := a.prepareBrowserLaunchContext(input, profile, bookmarks) if err != nil { return nil, err } effectiveProxy, acquiredProxyBridge, releaseProxyBridge, err := a.resolveBrowserStartProxy(input, profile) if err != nil { return nil, err } startReadyTimeout, startStableWindow := a.browserStartTimingSettings() maxStartAttempts := browserStartAttemptCount() totalReadyTimeout := time.Duration(maxStartAttempts) * startReadyTimeout restoreLastSession := browserRestoreLastSession(a.config) extensionDirs := a.browserMgr.EnabledExtensionDirsForProfile(input.ProfileID) defaultStartURLs := mergeStartURLs(browserDefaultStartURLs(a.config), bookmarkStartURLs(bookmarks)) launchTargets, deferredStartTargets := buildBrowserLaunchTargets( input.StartURLs, defaultStartURLs, input.SkipDefaultStartURLs, restoreLastSession, browserLightStartEnabled(a.config), ) assignedDebugPort, err := nextAvailablePort() if err != nil { startErr := fmt.Errorf("实例启动失败:本地调试端口分配失败。原因:%v。请关闭占用端口的程序后重试。", err) logger.New("Browser").Error("调试端口分配失败", logger.F("profile_id", input.ProfileID), logger.F("error", err.Error()), logger.F("reason", startErr.Error()), ) profile.LastError = startErr.Error() return nil, startErr } return &browserStartPlan{ profile: profile, chromeBinaryPath: chromeBinaryPath, userDataDir: userDataDir, extensionDirs: extensionDirs, args: buildBrowserLaunchArgs(profile, userDataDir, assignedDebugPort, effectiveProxy, extensionDirs, sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, launchTargets), deferredStartTargets: deferredStartTargets, effectiveProxy: effectiveProxy, acquiredProxyBridge: acquiredProxyBridge, releaseProxyBridge: releaseProxyBridge, assignedDebugPort: assignedDebugPort, startReadyTimeout: startReadyTimeout, startStableWindow: startStableWindow, maxStartAttempts: maxStartAttempts, totalReadyTimeout: totalReadyTimeout, }, nil } func (a *App) prepareBrowserLaunchContext(input browserStartInput, profile *BrowserProfile, bookmarks []BrowserBookmark) ([]string, []string, string, string, error) { log := logger.New("Browser") sanitizedProfileLaunchArgs, managedProfileArgs := sanitizeManagedLaunchArgs(profile.LaunchArgs) sanitizedExtraLaunchArgs, managedExtraArgs := sanitizeManagedLaunchArgs(input.ExtraLaunchArgs) logManagedLaunchArgOverrides(log, input.ProfileID, "profile.launchArgs", managedProfileArgs) logManagedLaunchArgOverrides(log, input.ProfileID, "start.extraLaunchArgs", managedExtraArgs) proxyChanged := a.browserMgr.ApplyDefaults(profile) if proxyChanged { _ = a.browserMgr.SaveProfiles() } chromeBinaryPath, err := a.browserMgr.ResolveChromeBinary(profile) if err != nil { startErr := fmt.Errorf("实例启动失败:%w", err) log.Error("内核路径解析失败", logger.F("profile_id", input.ProfileID), logger.F("error", err.Error()), logger.F("reason", startErr.Error()), ) profile.LastError = startErr.Error() return nil, nil, "", "", startErr } userDataDir := a.browserMgr.ResolveUserDataDir(profile) if err := os.MkdirAll(userDataDir, 0o755); err != nil { startErr := fmt.Errorf("实例启动失败:无法创建用户数据目录 %s。原因:%w。请检查目录权限或路径配置。", userDataDir, err) log.Error("用户数据目录创建失败", logger.F("profile_id", input.ProfileID), logger.F("dir", userDataDir), logger.F("error", err.Error()), logger.F("reason", startErr.Error()), ) profile.LastError = startErr.Error() return nil, nil, "", "", startErr } if err := browser.EnsureDefaultBookmarks(userDataDir, bookmarks); err != nil { log.Error("默认书签写入失败", logger.F("error", err.Error())) } if detection, ok := detectBrowserRuntimeByActivePort(userDataDir); ok && detection.DebugReady { a.markProfileRunningLocked(input.ProfileID, profile, nil, detection.PID, detection.DebugPort, true, "") log.Warn("检测到同一用户数据目录已有浏览器运行,已接管为当前实例状态", logger.F("profile_id", input.ProfileID), logger.F("user_data_dir", userDataDir), logger.F("pid", detection.PID), logger.F("debug_port", detection.DebugPort), ) if len(normalizeNonEmptyStrings(input.StartURLs)) == 0 && len(normalizeNonEmptyStrings(input.ExtraLaunchArgs)) == 0 { return nil, nil, "", "", errBrowserStartHandledByRecoveredRuntime } if err := a.openBrowserTabForRunningProfile(profile, input.ExtraLaunchArgs, input.StartURLs); err != nil { startErr := fmt.Errorf("实例已在运行,但新标签打开失败:%w", err) profile.LastError = startErr.Error() return nil, nil, "", "", startErr } return nil, nil, "", "", errBrowserStartHandledByRecoveredRuntime } if !browserRestoreLastSession(a.config) { if err := clearBrowserSessionRestoreData(userDataDir); err != nil { if terminated, terminateErr := terminateBrowserProcessesByUserDataDir(userDataDir, 5*time.Second); terminateErr == nil && terminated { log.Warn("会话缓存被旧浏览器进程占用,已结束占用进程并重试清理", logger.F("profile_id", input.ProfileID), logger.F("user_data_dir", userDataDir), ) if retryErr := clearBrowserSessionRestoreData(userDataDir); retryErr == nil { return sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, chromeBinaryPath, userDataDir, nil } else { err = retryErr } } else if terminateErr != nil { log.Warn("会话缓存清理失败后尝试结束占用进程失败", logger.F("profile_id", input.ProfileID), logger.F("user_data_dir", userDataDir), logger.F("error", terminateErr.Error()), ) } sessionDir := filepath.Join(userDataDir, "Default", "Sessions") startErr := fmt.Errorf("实例启动失败:无法清理上次会话缓存 %s。原因:%w。请关闭占用该目录的浏览器进程后重试。", sessionDir, err) log.Error("会话恢复缓存清理失败", logger.F("profile_id", input.ProfileID), logger.F("dir", sessionDir), logger.F("error", err.Error()), logger.F("reason", startErr.Error()), ) profile.LastError = startErr.Error() return nil, nil, "", "", startErr } } return sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, chromeBinaryPath, userDataDir, nil } func buildBrowserLaunchArgs(profile *BrowserProfile, userDataDir string, debugPort int, effectiveProxy string, extensionDirs []string, sanitizedProfileLaunchArgs []string, sanitizedExtraLaunchArgs []string, launchTargets []string) []string { args := []string{ fmt.Sprintf("--user-data-dir=%s", userDataDir), fmt.Sprintf("--remote-debugging-port=%d", debugPort), "--disable-session-crashed-bubble", } hasFingerprint := false for _, arg := range profile.FingerprintArgs { if strings.HasPrefix(arg, "--fingerprint=") { hasFingerprint = true break } } if !hasFingerprint { seed := 0 for _, char := range profile.ProfileId { seed = (seed << 5) - seed + int(char) } if seed < 0 { seed = -seed } args = append(args, fmt.Sprintf("--fingerprint=%d", seed)) } if effectiveProxy == "direct://" { args = append(args, "--no-proxy-server") } else if effectiveProxy != "" { args = append(args, fmt.Sprintf("--proxy-server=%s", effectiveProxy)) } if extensionArg := strings.Join(normalizeNonEmptyStrings(extensionDirs), ","); extensionArg != "" { args = append(args, fmt.Sprintf("--disable-extensions-except=%s", extensionArg)) args = append(args, fmt.Sprintf("--load-extension=%s", extensionArg)) } args = append(args, profile.FingerprintArgs...) args = append(args, sanitizedProfileLaunchArgs...) args = append(args, sanitizedExtraLaunchArgs...) return browser.BuildLaunchArgs(args, launchTargets) }