2025-07-21 00:09:21 +08:00
package http
import (
2026-03-23 00:05:41 +08:00
"fmt"
"os"
"path/filepath"
2025-07-21 00:09:21 +08:00
"runtime"
2026-03-23 00:05:41 +08:00
"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"
2025-07-23 20:30:29 +08:00
"github.com/go-rod/rod/lib/launcher/flags"
2025-07-21 00:09:21 +08:00
"github.com/ssgo/u"
2026-03-23 00:05:41 +08:00
"github.com/ysmood/fetchup"
2025-07-21 00:09:21 +08:00
)
type Chrome struct {
2025-07-23 20:30:29 +08:00
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 )
2025-07-23 20:30:29 +08:00
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 {
2026-03-23 00:05:41 +08:00
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
}
2026-03-23 00:05:41 +08:00
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
}
2026-03-23 00:05:41 +08:00
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 == "" {
2025-07-23 20:30:29 +08:00
// 使用本地Chrome
ch . launcher = launcher . New ( )
2026-03-23 00:05:41 +08:00
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
2025-07-23 20:30:29 +08:00
}
2026-03-23 00:05:41 +08:00
// 基础防御参数
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
}
2026-03-23 00:05:41 +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" )
}
2026-03-23 00:05:41 +08:00
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 ) )
}
2025-07-23 20:30:29 +08:00
}
}
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 )
2025-07-23 20:30:29 +08:00
} else {
ch . chromeURL = localChromeURL
if ch . chromePath == "" {
ch . chromePath = ch . launcher . Get ( flags . Bin )
}
2025-07-21 00:09:21 +08:00
}
}
2025-07-23 20:30:29 +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
}