//go:build !cgo // +build !cgo package client import ( "bufio" "context" _ "embed" "fmt" "net/url" "os" "path/filepath" "reflect" "runtime" "strconv" "strings" chromeRuntime "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" "github.com/ssgo/tool/watcher" "github.com/ssgo/u" ) type Webview struct { id uint64 ctx context.Context cancel context.CancelFunc isRunning bool // isAsync bool isDebug bool webRoot string watcher *watcher.Watcher html string url string closeChan chan bool width int height int title string evals []string bindFuncs map[string]any } var chromeVersion = -1 func (w *Webview) New(isDebug bool) { if chromeVersion == -1 { chromeVersion = getChromeVersion() } w.isDebug = isDebug } func (w *Webview) close() { // fmt.Println("====== w.Close()") if w.isRunning { w.isRunning = false // w.closeChan = make(chan bool, 1) // fmt.Println("====== w.w.Terminate()") w.cancel() // fmt.Println("====== w.w.Terminate() ok") // <-w.closeChan // w.closeChan = nil } } func (w *Webview) doClose() { // fmt.Println("====== w.doClose()") if w.watcher != nil { w.watcher.Stop() w.watcher = nil } windowsLock.Lock() delete(windows, w.id) n := len(windows) windowsLock.Unlock() // fmt.Println("====== w.w.Destroy()") if w.cancel != nil { w.cancel() } // fmt.Println("====== w.w.Destroy() ok") // 解除 w.Close 的等待 if w.closeChan != nil { w.closeChan <- true } // 解除 gojs.WaitAll 的等待 if waitChan != nil && n == 0 { waitChan <- true } } func (w *Webview) loadHtml(html string) { w.url = "data:text/html;charset=utf-8," + url.PathEscape(html) if w.ctx != nil { chromedp.Run(w.ctx, chromedp.Navigate(w.url)) } } func (w *Webview) loadURL(url string) { if url != "" { if strings.Contains(url, "://") { // chromedp.Run(w.ctx, chromedp.Navigate(url)) w.url = url } else { if !filepath.IsAbs(url) { curPath, _ := os.Getwd() url = filepath.Join(curPath, url) } w.webRoot = filepath.Dir(url) w.url = "file://" + url // chromedp.Run(w.ctx, chromedp.Navigate(url)) } } else { // chromedp.Run(w.ctx, chromedp.Navigate("about:blank")) w.url = "about:blank" } if w.ctx != nil { chromedp.Run(w.ctx, chromedp.Navigate(w.url)) } } func (w *Webview) reload() { if w.html != "" { w.loadHtml(w.html) } else { w.loadURL(w.url) } } func (w *Webview) setTitle(title string) { w.title = title if w.ctx != nil { chromedp.Run(w.ctx, chromedp.Evaluate(`document.title = "`+w.title+`"`, nil)) } } func (w *Webview) setSize(width int, height int, sizeMode string) { w.width = width w.height = height if w.ctx != nil { // chromedp.Run(w.ctx, chromedp.EmulateViewport(int64(w.width), int64(w.height))) } } func (w *Webview) eval(code string) any { if w.ctx == nil { w.evals = append(w.evals, code) return nil } var result any chromedp.Run(w.ctx, chromedp.Evaluate(code, &result)) return result } func (w *Webview) Dispatch(f func()) { // w.w.Dispatch(f) f() } func (w *Webview) Run() { // chromeVersion = 0 if chromeVersion > 0 { // 根据版本调整配置 opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", false), // 显示窗口 chromedp.Flag("window-size", fmt.Sprintf("%d,%d", w.width, w.height)), // 窗口尺寸 chromedp.Flag("app", w.url), // 直接启动到目标页 chromedp.Flag("disable-notifications", true), // 禁用通知 chromedp.Flag("no-first-run", true), // 跳过首次运行 chromedp.Flag("no-default-browser-check", true), // 禁用默认浏览器检查 chromedp.Flag("disable-component-update", true), // 禁用组件更新 chromedp.Flag("disable-background-networking", true), // 禁用后台网络 chromedp.Flag("disable-client-side-phishing-detection", true), // 禁用钓鱼检测 ) if chromeVersion >= 115 { // 新版本 opts = append(opts, chromedp.Flag("enable-automation", false), // chromedp.Flag("excludeSwitches", []string{"enable-automation"}), chromedp.Flag("useAutomationExtension", false), ) } else if chromeVersion >= 90 { // Chrome 90+ 参数 opts = append(opts, chromedp.Flag("disable-blink-features", "AutomationControlled"), ) } else { // Chrome 89 及以下参数 opts = append(opts, chromedp.Flag("disable-infobars", true), chromedp.Flag("disable-extensions", true), ) } allocCtx, cancel1 := chromedp.NewExecAllocator(context.Background(), opts...) ctx, cancel2 := chromedp.NewContext(allocCtx) w.ctx = ctx w.cancel = func() { cancel2() cancel1() } codes := []string{getDetectionScript(chromeVersion)} if w.evals != nil { codes = append(codes, strings.Join(w.evals, "\n")) } chromedp.ListenTarget(w.ctx, func(ev any) { if ev, ok := ev.(*chromeRuntime.EventBindingCalled); ok { if fn, ok := w.bindFuncs[ev.Name]; ok { fnV := reflect.ValueOf(fn) if fnV.Kind() == reflect.Func { var args []reflect.Value if ev.Payload != "" { argsV := u.FinalValue(reflect.ValueOf(u.UnJson(ev.Payload, nil))) if argsV.Kind() == reflect.Slice { for i := 0; i < fnV.Type().NumIn(); i++ { argType := fnV.Type().In(i) argValueP := reflect.New(argType) argValue := argValueP.Elem() if i < argsV.Len() { u.Convert(argsV.Index(i), argValueP) } args = append(args, argValue) } } go func() { _ = fnV.Call(args) }() } } } } }) actions := []chromedp.Action{} for k, _ := range w.bindFuncs { actions = append(actions, chromeRuntime.AddBinding(k)) codes = append(codes, fmt.Sprintf("window.__%s = %s; window.%s = function () { __%s(JSON.stringify(Array.from(arguments))) }", k, k, k, k)) } codes = append(codes, appCode) actions = append(actions, chromedp.Evaluate(strings.Join(codes, "\n"), nil)) exitChan := make(chan bool, 1) chromedp.Run(w.ctx, actions...) go func() { <-w.ctx.Done() exitChan <- true }() <-exitChan } else { if runtime.GOOS == "windows" { u.RunCommand("start", w.url) } else if runtime.GOOS == "darwin" { u.RunCommand("open", w.url) } else { u.RunCommand("xdg-open", w.url) } fmt.Println("URL: ", w.url) fmt.Println("Press Enter to exit...") reader := bufio.NewReader(os.Stdin) _, _ = reader.ReadString('\n') } } func (w *Webview) binds() { w.bindFuncs = map[string]any{} bindsLock.RLock() for k, v := range binds { w.bindFuncs[k] = v } bindsLock.RUnlock() w.bindFuncs["__closeWindow"] = w.close w.bindFuncs["__setTitle"] = w.setTitle w.bindFuncs["__setSize"] = w.setSize w.bindFuncs["__eval"] = w.eval } // 获取浏览器版本(精确到主版本号) func getChromeVersion() int { var ua string ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() if err := chromedp.Run(ctx, chromedp.Evaluate("navigator.userAgent", &ua), ); err != nil { return 0 } // 解析示例:Mozilla/5.0... Chrome/124.0.0.0... start := strings.Index(ua, "Chrome/") if start == -1 { return 0 } versionPart := ua[start+7:] end := strings.Index(versionPart, ".") if end == -1 { return 0 } version, _ := strconv.Atoi(versionPart[:end]) return version } // 生成版本自适应的检测脚本 func getDetectionScript(version int) string { baseJS := ` (() => { // 核心属性修改 Object.defineProperty(navigator, 'webdriver', { get: () => false, configurable: true }); // 通用内存清理 const legacyProps = ['$cdc', '__driver_evaluate']; const modernProps = ['_cdc', '__driver', '__automation']; [...legacyProps, ...modernProps].forEach(p => { try { delete window[p] } catch(e){} try { delete document[p] } catch(e){} });` if version >= 120 { // 修复点:将 JS 模板字符串改为普通字符串拼接 baseJS += ` // 伪造 Chrome 运行时 window.chrome = { runtime: { id: 'mock_extension', getManifest: () => ({ version: '1.0' }), getURL: function(path) { return 'chrome-extension://mock_extension/' + path; }, onMessage: { addListener: function(){} } } };` } baseJS += "\n})();" return baseJS + "\n" }