package http import ( "fmt" "os" "path/filepath" "runtime" "strings" "sync" "apigo.cc/gojs" "apigo.cc/gojs/goja" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/launcher/flags" "github.com/ssgo/u" "github.com/ysmood/fetchup" ) type Chrome struct { id string chromeURL string chromePath string launcher *launcher.Launcher browser *rod.Browser } var chromes = map[string]*Chrome{} var chromesLock sync.Mutex func (ch *Chrome) Close(vm *goja.Runtime) { // logger := gojs.GetLogger(vm) if ch.browser != nil { // ver, _ := ch.browser.Version() // logger.Info("关闭Chrome浏览器", "id", ch.id, "browser", ver.Product, "userAgent", ver.UserAgent, "chromeURL", ch.chromeURL, "chromePath", ch.chromePath) ch.browser.Close() ch.browser = nil } if ch.launcher != nil { ch.launcher.Cleanup() ch.launcher = nil } chromesLock.Lock() delete(chromes, ch.id) chromesLock.Unlock() } func CloseAllChrome(vm *goja.Runtime) { n := len(chromes) if n > 0 { for _, ch := range chromes { ch.Close(vm) } logger := gojs.GetLogger(vm) logger.Info("关闭所有未主动关闭的Chrome浏览器", "count", n) } } type ChromeInfo struct { ProtocolVersion string Product string Revision string UserAgent string JsVersion string ChromeURL string ChromePath string } type ChromeOption struct { ChromeURL string ChromePath string ChromeDownloadMirror string ChromeDownloadPath string ChromeOption []string ShowWindow bool HttpProxy string UserAgent string } func (ch *Chrome) Info(showWindow *bool, vm *goja.Runtime) (ChromeInfo, error) { ver, err := ch.browser.Version() if err != nil { return ChromeInfo{ ChromeURL: ch.chromeURL, ChromePath: ch.chromePath, }, err } return ChromeInfo{ ProtocolVersion: ver.ProtocolVersion, Product: ver.Product, Revision: ver.Revision, UserAgent: ver.UserAgent, JsVersion: ver.JsVersion, ChromeURL: ch.chromeURL, ChromePath: ch.chromePath, }, nil } var hostConf = map[string]struct { urlPrefix string zipName string }{ "darwin_amd64": {"Mac", "chrome-mac.zip"}, "darwin_arm64": {"Mac_Arm", "chrome-mac.zip"}, "linux_amd64": {"Linux_x64", "chrome-linux.zip"}, "windows_386": {"Win", "chrome-win.zip"}, "windows_amd64": {"Win_x64", "chrome-win.zip"}, }[runtime.GOOS+"_"+runtime.GOARCH] func StartChrome(opt *ChromeOption, vm *goja.Runtime) (*Chrome, error) { if opt == nil { opt = &ChromeOption{} } if opt.ChromeURL == "" { opt.ChromeURL = conf.ChromeURL } if opt.ChromePath == "" { opt.ChromePath = conf.ChromePath } if opt.ChromeDownloadMirror == "" { opt.ChromeDownloadMirror = conf.ChromeDownloadMirror } if opt.ChromeDownloadPath == "" { opt.ChromeDownloadPath = conf.ChromeDownloadPath } if opt.HttpProxy == "" { opt.HttpProxy = conf.HttpProxy } if opt.UserAgent == "" { opt.UserAgent = conf.UserAgent } if opt.ChromeOption == nil { opt.ChromeOption = conf.ChromeOption } // logger := gojs.GetLogger(vm) ch := &Chrome{} ch.id = u.UniqueId() ch.browser = rod.New() ch.chromeURL = opt.ChromeURL if opt.ChromeURL == "" { // 使用本地Chrome ch.launcher = launcher.New() if opt.ChromePath != "" { ch.launcher.Bin(opt.ChromePath) ch.chromePath = opt.ChromePath } else if path, has := launcher.LookPath(); has { ch.launcher.Bin(path) ch.chromePath = path } else { // 自动下载Chrome lb := launcher.NewBrowser() // 默认优先级:淘宝(NPM) -> Playwright -> Google lb.Hosts = []launcher.Host{launcher.HostNPM, launcher.HostPlaywright, launcher.HostGoogle} if opt.ChromeDownloadMirror != "" { // 定义私有镜像 Host 函数 var customHost launcher.Host = func(rev int) string { // 构造逻辑完全对齐 rod 官方:Prefix/OS_Arch/Revision/ZipName return fmt.Sprintf("%s/%s/%d/%s", strings.TrimSuffix(opt.ChromeDownloadMirror, "/"), // 去掉末尾的斜杠,防止拼接出双斜杠 hostConf.urlPrefix, rev, hostConf.zipName, ) } // 将自定义镜像插入到最前面 lb.Hosts = append([]launcher.Host{customHost}, lb.Hosts...) } var path string var err error // --- 核心修改区:只有指定了路径才走手动逻辑 --- if opt.ChromeDownloadPath != "" { rev := lb.Revision // 构造存放目录和最终二进制路径 destDir := filepath.Join(opt.ChromeDownloadPath, fmt.Sprintf("chromium-%d", rev)) var binPath string switch runtime.GOOS { case "darwin": // macOS binPath = filepath.Join(destDir, "Chromium.app/Contents/MacOS/Chromium") case "windows": // Windows binPath = filepath.Join(destDir, "chrome.exe") default: // linux binPath = filepath.Join(destDir, "chrome") } // --- 检查本地是否存在 (实现跳过下载) --- if _, err = os.Stat(binPath); err == nil { path = binPath } else { // --- 手动触发 fetchup 下载流程 --- urls := []string{} for _, host := range lb.Hosts { urls = append(urls, host(rev)) } // 使用你发现的 fetchup 逻辑,直接下载到指定的 destDir fu := fetchup.New(destDir, urls...) // 如果你设置了代理,也可以传给 fu.HttpClient err = fu.Fetch() if err == nil { // 自动处理解压后的目录层级缩减 _ = fetchup.StripFirstDir(destDir) path = binPath // 修正 Linux 权限 if runtime.GOOS != "windows" { _ = os.Chmod(path, 0755) } } } } else { // 如果没有指定自定义路径,直接走 Rod 默认逻辑(会走 ENV 和默认缓存) path, err = lb.Get() } if err != nil { ch.Close(vm) return nil, gojs.Err(fmt.Errorf("自动下载浏览器失败: %w", err)) } ch.launcher.Bin(path) ch.chromePath = path } // 基础防御参数 ch.launcher. Set("disable-dev-shm-usage"). Set("single-process"). // 保留你原有的逻辑 Set("disable-blink-features", "AutomationControlled"). Set("no-sandbox"). Set("disable-infobars"). Delete("enable-automation") // 彻底删除自动化控制标志 // --- 动态 Headless 逻辑 --- if opt.ShowWindow { ch.launcher.Headless(false) } else { // 使用新版无头模式,不要调用 .Headless(true) ch.launcher.Set("headless", "new") } // --- 动态 User-Agent 优化 --- if opt.UserAgent != "" { ch.launcher.Set("user-agent", opt.UserAgent) } else { // 建议版本号保持在 130+,目前 146 有点过于领先(当前稳定版大约在 134 左右) const chromeVersion = "134.0.0.0" switch runtime.GOOS { case "darwin": // macOS ch.launcher.Set("user-agent", fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", chromeVersion)) case "linux": // 建议 Linux 下还是用 Linux 的 UA,减少被识别为“伪装者”的风险 ch.launcher.Set("user-agent", fmt.Sprintf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", chromeVersion)) default: // windows ch.launcher.Set("user-agent", fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", chromeVersion)) } } if opt.HttpProxy != "" { ch.launcher.Proxy(opt.HttpProxy) } // 操作系统特定兼容 switch runtime.GOOS { case "linux": ch.launcher.Set("disable-setuid-sandbox") case "windows": ch.launcher.Set("disable-features=RendererCodeIntegrity") } if opt.ChromeOption != nil { for _, opt := range opt.ChromeOption { a := u.SplitTrimN(opt, "=", 2) if len(a) == 2 { ch.launcher.Set(flags.Flag(a[0]), a[1]) } else { ch.launcher.Set(flags.Flag(opt)) } } } if localChromeURL, err := ch.launcher.Launch(); err != nil { ch.Close(vm) return nil, gojs.Err(err) } else { ch.chromeURL = localChromeURL if ch.chromePath == "" { ch.chromePath = ch.launcher.Get(flags.Bin) } } } ch.browser.ControlURL(ch.chromeURL) if err := ch.browser.Connect(); err != nil { return nil, gojs.Err(err) } // ver, _ := ch.browser.Version() // logger.Info("启动Chrome浏览器", "id", ch.id, "browser", ver.Product, "userAgent", ver.UserAgent, "chromeURL", ch.chromeURL, "chromePath", ch.chromePath) chromesLock.Lock() chromes[ch.id] = ch chromesLock.Unlock() return ch, nil }