308 lines
8.4 KiB
Go
308 lines
8.4 KiB
Go
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
|
||
}
|