http/chrome.go

308 lines
8.4 KiB
Go
Raw Permalink Normal View History

2025-07-21 00:09:21 +08:00
package http
import (
"fmt"
"os"
"path/filepath"
2025-07-21 00:09:21 +08:00
"runtime"
"strings"
2025-07-21 00:09:21 +08:00
"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"
2025-07-21 00:09:21 +08:00
"github.com/ssgo/u"
"github.com/ysmood/fetchup"
2025-07-21 00:09:21 +08:00
)
type Chrome struct {
id string
chromeURL string
chromePath string
launcher *launcher.Launcher
browser *rod.Browser
2025-07-21 00:09:21 +08:00
}
var chromes = map[string]*Chrome{}
var chromesLock sync.Mutex
2025-07-22 20:45:03 +08:00
func (ch *Chrome) Close(vm *goja.Runtime) {
2025-07-24 19:13:07 +08:00
// logger := gojs.GetLogger(vm)
2025-07-21 00:09:21 +08:00
if ch.browser != nil {
2025-07-24 19:13:07 +08:00
// ver, _ := ch.browser.Version()
// logger.Info("关闭Chrome浏览器", "id", ch.id, "browser", ver.Product, "userAgent", ver.UserAgent, "chromeURL", ch.chromeURL, "chromePath", ch.chromePath)
2025-07-21 00:09:21 +08:00
ch.browser.Close()
ch.browser = nil
}
if ch.launcher != nil {
ch.launcher.Cleanup()
ch.launcher = nil
}
chromesLock.Lock()
delete(chromes, ch.id)
chromesLock.Unlock()
}
2025-07-22 20:45:03 +08:00
func CloseAllChrome(vm *goja.Runtime) {
2025-07-21 00:09:21 +08:00
n := len(chromes)
if n > 0 {
for _, ch := range chromes {
2025-07-22 20:45:03 +08:00
ch.Close(vm)
2025-07-21 00:09:21 +08:00
}
2025-07-22 20:45:03 +08:00
logger := gojs.GetLogger(vm)
logger.Info("关闭所有未主动关闭的Chrome浏览器", "count", n)
2025-07-21 00:09:21 +08:00
}
}
2025-07-24 19:13:07 +08:00
type ChromeInfo struct {
ProtocolVersion string
Product string
Revision string
UserAgent string
JsVersion string
ChromeURL string
ChromePath string
}
2025-07-24 19:23:38 +08:00
type ChromeOption struct {
ChromeURL string
ChromePath string
ChromeDownloadMirror string
ChromeDownloadPath string
ChromeOption []string
ShowWindow bool
HttpProxy string
UserAgent string
2025-07-24 19:23:38 +08:00
}
2025-07-24 19:13:07 +08:00
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]
2025-07-24 19:23:38 +08:00
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
2025-07-24 19:23:38 +08:00
}
if opt.ChromeOption == nil {
opt.ChromeOption = conf.ChromeOption
}
2025-07-24 19:13:07 +08:00
// logger := gojs.GetLogger(vm)
2025-07-21 00:09:21 +08:00
ch := &Chrome{}
2025-07-22 20:45:03 +08:00
ch.id = u.UniqueId()
2025-07-21 00:09:21 +08:00
ch.browser = rod.New()
2025-07-24 19:23:38 +08:00
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")
2025-07-24 19:23:38 +08:00
}
// --- 动态 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)
}
// 操作系统特定兼容
2025-07-21 00:09:21 +08:00
switch runtime.GOOS {
case "linux":
ch.launcher.Set("disable-setuid-sandbox")
case "windows":
ch.launcher.Set("disable-features=RendererCodeIntegrity")
}
2025-07-24 19:23:38 +08:00
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 {
2025-07-22 20:45:03 +08:00
ch.Close(vm)
2025-07-21 00:09:21 +08:00
return nil, gojs.Err(err)
} else {
ch.chromeURL = localChromeURL
if ch.chromePath == "" {
ch.chromePath = ch.launcher.Get(flags.Bin)
}
2025-07-21 00:09:21 +08:00
}
}
ch.browser.ControlURL(ch.chromeURL)
2025-07-21 00:09:21 +08:00
if err := ch.browser.Connect(); err != nil {
return nil, gojs.Err(err)
}
2025-07-24 19:13:07 +08:00
// ver, _ := ch.browser.Version()
// logger.Info("启动Chrome浏览器", "id", ch.id, "browser", ver.Product, "userAgent", ver.UserAgent, "chromeURL", ch.chromeURL, "chromePath", ch.chromePath)
2025-07-21 00:09:21 +08:00
chromesLock.Lock()
chromes[ch.id] = ch
chromesLock.Unlock()
return ch, nil
}