Go测试代码

package test

import (
    "context"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "slices"
    "testing"
    "time"

    "github.com/chromedp/cdproto/cdp"
    "github.com/chromedp/cdproto/network"
    "github.com/chromedp/chromedp"
)
// userAgent 浏览器的UA头
var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0"

// fingerprintJSStr 随机浏览器指纹信息
var fingerprintJSStr = `
(() => {
  try {
    // === 随机生成函数(轻度安全随机) ===
    const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
    const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
    const randFloat = (min, max) => Math.random() * (max - min) + min;

    // === 固定信息 ===
    const FIXED_LANGS = ['zh-CN', 'zh'];
    const FIXED_LANG = 'zh-CN';
    const FIXED_PLATFORM = 'Win32';

    // === 随机信息 ===
    const RAND_HW_CONCURRENCY = pick([4, 6, 8, 12]);
    const RAND_DEVICE_MEMORY = pick([4, 8, 12, 16]);
    const RAND_GL_VENDOR = pick(['Intel Inc.', 'NVIDIA Corporation', 'ATI Technologies Inc.']);
    const RAND_GL_RENDERER = pick([
      'Intel(R) UHD Graphics 630',
      'NVIDIA GeForce GTX ' + randInt(1050, 3090),
      'AMD Radeon RX ' + randInt(5600, 7900)
    ]);

    // 1) navigator.webdriver
    Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });

    // 2) 语言固定为中文
    Object.defineProperty(navigator, 'languages', { get: () => FIXED_LANGS.slice(), configurable: true });
    Object.defineProperty(navigator, 'language', { get: () => FIXED_LANG, configurable: true });

    // 3) plugins & mimeTypes
    const fakePlugins = [
      { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
      { name: 'Widevine Content Decryption Module', filename: 'widevinecdm', description: 'Widevine CDM' }
    ];
    const pluginArray = { length: fakePlugins.length };
    fakePlugins.forEach((p, i) => pluginArray[i] = p);
    pluginArray.item = (i) => pluginArray[i];
    pluginArray.namedItem = (name) => fakePlugins.find(p => p.name === name) || null;
    Object.defineProperty(navigator, 'plugins', { get: () => pluginArray, configurable: true });
    Object.defineProperty(navigator, 'mimeTypes', { get: () => ({ length: 0, item: () => null }), configurable: true });

    // 4) permissions.query 修复
    const originalPermissionsQuery = window.navigator.permissions && window.navigator.permissions.query;
    if (originalPermissionsQuery) {
      const nativeQuery = originalPermissionsQuery.bind(window.navigator.permissions);
      window.navigator.permissions.query = (parameters) => {
        if (parameters && parameters.name === 'notifications') {
          return Promise.resolve({ state: Notification.permission });
        }
        return nativeQuery(parameters);
      };
    }

    // 5) 硬件信息随机 + 平台固定
    Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => RAND_HW_CONCURRENCY, configurable: true });
    Object.defineProperty(navigator, 'deviceMemory', { get: () => RAND_DEVICE_MEMORY, configurable: true });
    Object.defineProperty(navigator, 'platform', { get: () => FIXED_PLATFORM, configurable: true });

    // 6) chrome.runtime 伪造
    window.chrome = window.chrome || {};
    window.chrome.runtime = window.chrome.runtime || {};

    // 7) WebGL vendor/renderer 随机
    const getParameter = WebGLRenderingContext.prototype.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
      if (parameter === 37445) return RAND_GL_VENDOR;
      if (parameter === 37446) return RAND_GL_RENDERER;
      return getParameter.call(this, parameter);
    };

    // 8) Canvas 指纹扰动(轻度随机)
    const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
    HTMLCanvasElement.prototype.toDataURL = function() {
      try {
        const ctx = this.getContext('2d');
        if (ctx) {
          const alpha = randFloat(0.00005, 0.001);
          ctx.fillStyle =  "rgba(0,0,0," + alpha + ")";
          ctx.fillRect(randInt(0, 2), randInt(0, 2), 1, 1);
        }
      } catch (e) {}
      return origToDataURL.apply(this, arguments);
    };

    const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
    CanvasRenderingContext2D.prototype.getImageData = function() {
      try {
        const img = origGetImageData.apply(this, arguments);
        if (img && img.data && img.data.length >= 4) {
          const delta = randInt(-1, 1);
          img.data[3] = Math.max(0, Math.min(255, img.data[3] + delta));
        }
        return img;
      } catch (e) {
        return origGetImageData.apply(this, arguments);
      }
    };

    // 9) Battery
    if (navigator.getBattery) {
      const level = parseFloat((randFloat(0.6, 1.0)).toFixed(2));
      navigator.getBattery = () => Promise.resolve({
        charging: true,
        chargingTime: randInt(0, 10),
        dischargingTime: Infinity,
        level: level
      });
    }

    // 10) 函数字符串隐藏 webdriver
    const re = /function\s+get\s+webdriver|webdriver|HeadlessChrome/ig;
    const origToString = Function.prototype.toString;
    Function.prototype.toString = function() {
      try {
        const s = origToString.apply(this, arguments);
        if (re.test(s)) return 'function () { [native code] }';
        return s;
      } catch (e) {
        return origToString.apply(this, arguments);
      }
    };

    // 11) iframe navigator 一致化
    const descriptor = Object.getOwnPropertyDescriptor(navigator.__proto__, 'userAgent') || {};
    if (!descriptor || !descriptor.get) {
      Object.defineProperty(navigator, 'userAgent', {
        get: () => navigator.userAgent.replace('HeadlessChrome', 'Chrome'),
        configurable: true
      });
    }

    // 12) 删除 webdriver
    delete navigator.__proto__.webdriver;

    // === 诊断输出(调试时可查看) ===
    window.__fingerprint_profile = {
      lang: FIXED_LANG,
      platform: FIXED_PLATFORM,
      hwConcurrency: RAND_HW_CONCURRENCY,
      deviceMemory: RAND_DEVICE_MEMORY,
      glVendor: RAND_GL_VENDOR,
      glRenderer: RAND_GL_RENDERER
    };

  } catch (e) {
    // ignore
  }
})();
`

func TestDoubao(t *testing.T) {
    ctx := context.Background()

    chromedpOptions := []chromedp.ExecAllocatorOption{
        chromedp.Flag("headless", false),            // debug使用 false
        chromedp.Flag("disable-hang-monitor", true), // 禁用页面无响应检测

        // 核心:禁用自动化指示器
        chromedp.Flag("enable-automation", false),
        chromedp.Flag("useAutomationExtension", false),
        chromedp.Flag("disable-blink-features", "AutomationControlled"),
        // 辅助:增强伪装
        chromedp.UserAgent(userAgent),
        chromedp.Flag("disable-web-security", false),
        chromedp.Flag("ignore-certificate-errors", false),
        // 随机化窗口大小,避免所有实例千篇一律
        chromedp.WindowSize(1920+rand.Intn(200), 1080+rand.Intn(200)),
    }

    //初始化参数,先传一个空的数据
    chromedpOptions = append(chromedp.DefaultExecAllocatorOptions[:], chromedpOptions...)
    //allocatorContext, cancel1 := chromedp.NewExecAllocator(ctx, chromedpOptions...)
    allocatorContext, cancel1 := chromedp.NewRemoteAllocator(ctx, "ws://10.0.0.131:8222")
    defer cancel1()
    chromeCtx, cancel2 := chromedp.NewContext(allocatorContext)
    defer cancel2()
    // 执行一个空task, 用提前创建Chrome实例
    //chromedp.Run(chromeCtx, make([]chromedp.Action, 0, 1)...)

    //创建一个上下文,超时时间为300s
    chromeCtx, cancel3 := context.WithTimeout(chromeCtx, time.Duration(300)*time.Second)
    defer cancel3()

    // 监听deepseek的sse请求
    sseStatus := make(chan int, 1)
    var content string
    var requestIDs []network.RequestID
    // 添加监听器
    chromedp.ListenTarget(chromeCtx, func(ev interface{}) {
        switch ev := ev.(type) {
        case *network.EventRequestWillBeSent: //SSE请求发送
            uri, _ := url.Parse(ev.Request.URL)
            if uri.Path != "/samantha/chat/completion" {
                break
            }
            if ev.Request.Method != "POST" {
                break
            }
            requestIDs = append(requestIDs, ev.RequestID)

        case *network.EventLoadingFinished: //SSE事件结束
            i := slices.Index(requestIDs, ev.RequestID)
            if i < 0 {
                break
            }
            requestIDs = slices.Delete(requestIDs, i, i+1)
            fc := chromedp.FromContext(chromeCtx)
            ctx := cdp.WithExecutor(chromeCtx, fc.Target)
            go func() { //使用协程,避免错误
                bs, err := network.GetResponseBody(ev.RequestID).Do(ctx)
                if err != nil {
                    return
                }
                //获取SSE的整体返回值
                content = string(bs)
                sseStatus <- 1
            }()
        }
    })

    screenshotLogin := make([]byte, 0)
    screenshotSession := make([]byte, 0)

    // 每次重启容器,作为匿名用户访问豆包
    resp, err := http.Post("http://10.0.0.131:2375/containers/chromedp-doubao/restart", "application/x-www-form-urlencoded", nil)
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    defer resp.Body.Close()
    

    time.Sleep(3 * time.Second)

    err = chromedp.Run(chromeCtx, chromedp.Tasks{
        
       //chromedp.Evaluate(`Object.defineProperty(navigator, 'webdriver', {get: () => undefined})`, nil),
        chromedp.ActionFunc(func(ctx context.Context) error {
            _, err := page.AddScriptToEvaluateOnNewDocument(fingerprintJSStr).Do(ctx)
            return err
        }),

        // 启用网络事件监听,这是关键一步
        network.Enable(),

        // 覆盖 navigator.userAgent 等
        emulation.SetUserAgentOverride(userAgent),
        // 可同时设置额外请求头
        network.SetExtraHTTPHeaders(network.Headers{"Accept-Language": "zh-CN,zh;q=0.9", "User-Agent": userAgent}),

        //指定分辨率的窗口
        emulation.SetDeviceMetricsOverride(1920+rand.Intn(200), 1080+rand.Intn(200), 1.0, false).
            WithScreenOrientation(&emulation.ScreenOrientation{
                Type:  emulation.OrientationTypePortraitPrimary,
                Angle: 0,
            }),

        // 导航到登录页面
        chromedp.Navigate("https://www.doubao.com/chat/"),
        //等待三秒
        chromedp.Sleep(3 * time.Second),
        /*
            // 判断页面是否是已经登录
            chromedp.ActionFunc(func(ctx context.Context) error {
                // 检查是否在登录页面(通过查找登录相关元素).使用相同的chromeCtx
                var exists bool
                err := chromedp.Run(chromeCtx, chromedp.EvaluateAsDevTools(`document.evaluate('//*[text()=\"密码登录\"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue !== null`, &exists))
                if err != nil {
                    return err
                }

                if !exists {
                    return nil
                }

                // 如果找到登录元素,执行登录流程.使用相同的chromeCtx
                return chromedp.Run(chromeCtx, chromedp.Tasks{

                    // 点击密码登录标签页
                    chromedp.WaitReady(`//*[text()='密码登录']`, chromedp.BySearch),
                    chromedp.Click(`//*[text()='密码登录']`, chromedp.BySearch),

                    //输入账号
                    chromedp.WaitReady(`//*[@placeholder='请输入手机号/邮箱地址']`, chromedp.BySearch),
                    chromedp.SendKeys(`//*[@placeholder='请输入手机号/邮箱地址']`, "账号", chromedp.BySearch),

                    //输入密码
                    chromedp.WaitReady(`//*[@placeholder='请输入密码']`, chromedp.BySearch),
                    chromedp.SendKeys(`//*[@placeholder='请输入密码']`, "密码", chromedp.BySearch),

                    //点击登录
                    chromedp.WaitReady(`//*[text()='登录']`, chromedp.BySearch),
                    chromedp.Click(`//*[text()='登录']`, chromedp.BySearch),

                    // 等待登录完成
                    chromedp.Sleep(3 * time.Second),
                })

            }),
        */

        // 登录截屏
        chromedp.CaptureScreenshot(&screenshotLogin),

        //1.输入问题
        chromedp.SendKeys(`textarea[data-testid="chat_input_input"]`, "今天的热点新闻", chromedp.ByQuery),
        //等待三秒
        chromedp.Sleep(3 * time.Second),

        // 2. 等待按钮处于可点击状态 (aria-disabled="false" 且没有 disabled 属性)
        chromedp.WaitVisible(`button#flow-end-msg-send[aria-disabled="false"]:not([disabled])`, chromedp.ByQuery),

        // 3. 点击按钮
        chromedp.Click(`button#flow-end-msg-send`, chromedp.ByQuery),

        // 等待大模型回复,最多等待3分钟
        chromedp.ActionFunc(func(ctx context.Context) error {
            // 设置超时时间为180秒
            select {
            case <-sseStatus: // 成功状态
                return nil
            case <-time.After(180 * time.Second):
                return nil
            }
        }),
        // 会话截屏
        chromedp.CaptureScreenshot(&screenshotSession),
    })

    fmt.Println(err)
    if len(screenshotLogin) > 0 {
        os.Remove("screenshotLogin.png")
        os.WriteFile("screenshotLogin.png", screenshotLogin, 0644)
    }

    if len(screenshotSession) > 0 {
        os.Remove("screenshotSession.png")
        os.WriteFile("screenshotSession.png", screenshotSession, 0644)
    }

    fmt.Println(content)

}

docker compose

services:
  chromedp:
    image: chromedp/headless-shell:142.0.7420.3
    container_name: chromedp
    restart: unless-stopped
    command: 
      - "--headless=new"
      - "--window-size=1920,1080"
      - "--no-sandbox"
      - "--disable-setuid-sandbox"
      - "--disable-background-timer-throttling"
      - "--disable-backgrounding-occluded-windows"
      - "--disable-renderer-backgrounding"
      - "--disable-features=VizDisplayCompositor"
      - "--enable-unsafe-swiftshader"
      - "--disable-hang-monitor"
      - "--disable-automation"
      - "--disable-extensions"
      - "--disable-blink-features=AutomationControlled"
      - "--disable-web-security=false"
      - "--ignore-certificate-errors=false"
    shm_size: 4g
    #extra_hosts:
    #  - "www.doubao.com:10.0.0.131"
    environment:
      TZ: Asia/Shanghai
      LANG: zh_CN.UTF-8
      LC_ALL: zh_CN.UTF-8
    ports:
      - "8222:9222"
    deploy:
      resources:
        limits:
          cpus: '4'
          memory: 16G
        reservations:
          cpus: '4'
          memory: 8G  

中文乱码

### 豆包的 Content-Type text/event-stream 没有 charset=utf-8,造成乱码.(CDP会严格按照协议执行解析,无法修改Content-Type)
    ### 指定 www.doubao.com 的Nginx解析IP, 自签https证书
    #extra_hosts:
    #  - "www.doubao.com:10.0.0.131"

        location /samantha/chat/completion {

        ## 设置UTF-8编码
            add_header Content-Type "text/event-stream; charset=utf-8";

            proxy_set_header                Host                            $host;
            proxy_set_header                X-Real-IP                       $remote_addr;
            proxy_set_header                X-Forwarded-For                 $proxy_add_x_forwarded_for;

            # 取消缓冲
            proxy_buffering off;
            # 关闭代理缓存
            proxy_cache off;
            # 代理到真实的豆包服务
            proxy_pass    https://www.doubao.com;

       }
        location / {
            proxy_set_header                Host                            $host;
            proxy_set_header                X-Real-IP                       $remote_addr;
            proxy_set_header                X-Forwarded-For                 $proxy_add_x_forwarded_for;

            # 取消缓冲
            proxy_buffering off;
            # 关闭代理缓存
            proxy_cache off;
            # 代理到真实的豆包服务
            proxy_pass    https://www.doubao.com;
        }