You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何唯一标识Node.js自动化场景下的Puppeteer浏览器实例?

解决多Puppeteer实例下验证码提交的会话定位问题

看起来你遇到的核心问题是全局变量导致的实例冲突——你用了全局的browser和page变量,当多个用户同时发起请求时,新创建的实例会直接覆盖旧的,所以后续提交验证码时自然会定位到最后创建的那个实例,而非目标用户的会话。

要解决这个问题,我们需要给每个用户的自动化流程分配一个唯一会话ID,并把每个会话对应的Puppeteer实例(browser、page)和用户信息绑定起来,用一个管理器来存储这些会话。这样提交验证码时,只要带上对应的会话ID,就能精准定位到目标实例。

下面是具体的实现方案:

步骤1:创建会话管理器

我们可以用Map来存储会话,键是唯一会话ID,值是包含该会话的browser、page、用户邮箱等信息的对象,这样可以快速通过ID查找对应的实例。

// 全局会话管理器,存储每个用户的会话数据
const sessionManager = new Map();

// 生成唯一会话ID的工具函数
function generateSessionId() {
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}

步骤2:修改startBrowser函数,返回实例而非赋值全局变量

去掉原来的全局browser和page,让函数返回创建好的浏览器和页面实例,方便后续绑定到对应会话:

async function startBrowser(proxyUrl, proxyUn, proxyPw) {
  let browser;
  if (!proxyUrl || !proxyUn || !proxyPw) {
    browser = await puppeteer.launch({
      headless: false,
      ignoreDefaultArgs: ["--enable-automation"],
      defaultViewport: null,
      args: ["--no-sandbox", "--disable-setuid-sandbox", "--start-maximized", '--window-size=1920,1080', "--disable-gpu", "--disable-dev-profile"]
    });
  } else {
    const oldProxyUrl = `http://${proxyUn}:${proxyPw}@${proxyUrl}`;
    const newProxyUrl = await proxyChain.anonymizeProxy(oldProxyUrl);
    browser = await puppeteer.launch({
      headless: false,
      ignoreDefaultArgs: ["--enable-automation"],
      defaultViewport: null,
      args: ["--no-sandbox", "--disable-setuid-sandbox", "--start-maximized", '--window-size=1920,1080', "--disable-gpu", "--disable-dev-profile", `--proxy-server=${newProxyUrl}`]
    });
  }

  // 浏览器断开时自动清理会话,避免内存泄漏
  browser.on('disconnected', () => {
    for (const [sessionId, session] of sessionManager.entries()) {
      if (session.browser === browser) {
        sessionManager.delete(sessionId);
        break;
      }
    }
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });
  page.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36");
  
  return { browser, page };
}

步骤3:修改playTest函数,生成会话ID并存储会话数据

在启动浏览器后,生成会话ID,把browser、page、用户邮箱等信息存入会话管理器,当检测到需要验证码时,把会话ID返回给前端:

async function playTest(url, res, email, password) { // 这里需要传入用户的email和password
  try {
    let browser, page;
    try {
      // 假设proxy参数从请求中获取,根据你的实际逻辑调整
      const { browser: newBrowser, page: newPage } = await startBrowser(proxyUrl, proxyUn, proxyPw);
      browser = newBrowser;
      page = newPage;
      await page.setDefaultNavigationTimeout(32400000);
    } catch (e) {
      proxyFailedFlag = true;
      console.error(new Date().toLocaleString() + ': ', e);
      return res.status(400).json({ status: 'failure', info: 'Invalid Proxy' }).end();
    }

    try {
      console.log(new Date().toLocaleString() + ': ', 'connecting login page ...');
      await page.goto(url);
    } catch (e) {
      proxyFailedFlag = true;
      await browser.close();
      console.error(new Date().toLocaleString() + ': ', e);
      return res.status(400).json({ status: 'failure', info: 'Invalid Proxy' }).end();
    }

    await waitTillHTMLRendered(page);
    await page.setViewport({ width: 1400, height: 966 });

    // 生成唯一会话ID
    const sessionId = generateSessionId();
    // 存储会话数据:包括browser、page、用户身份信息
    sessionManager.set(sessionId, {
      browser,
      page,
      email,
      password
    });

    // 调用playing时传入page、res和sessionId,方便返回会话ID
    await playing(page, res, sessionId);
  } catch (e) {
    console.error(new Date().toLocaleString() + ': ', e);
    res.status(500).json({ status: 'failure', info: 'Internal Server Error' }).end();
  }
}

步骤4:修改playing函数,在需要验证码时返回会话ID

调整playing函数,不再依赖全局page,而是接收传入的page和sessionId,当检测到需要验证码或PIN码时,把sessionId包含在响应中返回给前端:

const playing = async (page, res, sessionId) => {
  console.log(new Date().toLocaleString() + ': ', 'waiting for login form ...');
  if (await page.$(ACCEPT_COOKIES_SELECTOR) !== null) {
    const accept_elm = await page.$(ACCEPT_COOKIES_SELECTOR);
    await accept_elm.click({ clickCount: 1 });
  }

  await page.waitForSelector(USERNAME_SELECTOR, { visible: true });
  await page.click(USERNAME_SELECTOR);
  // 从会话中获取用户的邮箱和密码
  const { email, password } = sessionManager.get(sessionId);
  await page.keyboard.type(email);
  await page.click(PASSWORD_SELECTOR);
  await page.keyboard.type(password);

  const m_login_elm = await page.$(M_LOGIN_SELECTOR);
  await m_login_elm.click({ clickCount: 1 });
  console.log(new Date().toLocaleString() + ': ', 'logging in ...');

  try {
    await page.waitForNavigation();
    const pageURL = page.url();
    console.log("page url: ", pageURL);

    if (pageURL === 'https://www.targetwebsite.com/feed/') {
      loginFailedFlag = false;
      const accountElm = await page.$(PROFILE_NAME_SELECTOR);
      const accountName = await page.evaluate(accountElm => accountElm.textContent.trim(), accountElm);
      console.log("Login success", accountName);
      // 登录成功后清理会话
      sessionManager.delete(sessionId);
      return res.status(200).json({ status: 'success', info: 'Login Completed', accountName }).end();
    } else if (pageURL === "https:/www.targetwebsite.com/login-submit") {
      console.log("Exception Login Submit Form!");
      if (await page.$(NOT_REMEMBER_BTN_SELECTOR) !== null) {
        const notRememberBtn = await page.$(NOT_REMEMBER_BTN_SELECTOR);
        await notRememberBtn.click({ clickCount: 1 });
      } else {
        throw new Error('Login fail!');
      }
    } else {
      const pageURLParts = pageURL.split("challenge");
      if (pageURLParts[0] === "https://www.targetwebsite.com/captcha/") {
        loginFailedFlag = true;
        // 返回会话ID给前端,让用户提交验证码时带上
        return res.status(400).json({ 
          status: 'failure', 
          info: 'Captcha Required',
          sessionId 
        }).end();
      }
      console.log("Pin code is required");
      // 返回会话ID,等待用户提交PIN码
      return res.status(400).json({ 
        status: 'pending', 
        info: 'Awaiting IP Code',
        sessionId 
      }).end();
    }
  } catch (error) {
    loginFailedFlag = true;
    sessionManager.delete(sessionId);
    return res.status(400).json({ status: 'failure', info: 'Invalid Login' }).end();
  }

  if (await page.$(PIN_INPUT_SELECTOR) !== null) {
    console.log("pin code required")
    // 这里已提前返回会话ID,无需重复处理
  } else {
    console.log('not found Pin Code step');
    await waitTillHTMLRendered(page);
    if (await page.$(RE_ACCEPT_COOKIES_SELECTOR) !== null) {
      const accept_elm = await page.$(RE_ACCEPT_COOKIES_SELECTOR);
      await accept_elm.click({ clickCount: 1 });
    }
    await getCookies(page); // 修改getCookies,接收page参数
    sessionManager.delete(sessionId);
    res.status(200).json({ status: 'success', info: 'Process Completed' }).end();
  }
}

步骤5:修改验证码提交API,通过会话ID定位实例

调整handleIpProcess函数,接收sessionId、ip_code作为参数,从会话管理器中取出对应的page进行操作:

async function handleIpProcess(sessionId, ip_code) {
  // 通过会话ID获取对应的会话数据
  const session = sessionManager.get(sessionId);
  if (!session) {
    throw new Error('Invalid or expired session');
  }
  const { page, browser } = session;

  try {
    await waitTillHTMLRendered(page);
    await page.click(PIN_INPUT_SELECTOR);
    await page.keyboard.type(ip_code);
    await page.waitForSelector(PIN_SUBMIT_SELECTOR, { visible: true });
    await page.click(PIN_SUBMIT_SELECTOR);
    
    // 等待验证完成
    await page.waitForNavigation();
    const pageURL = page.url();
    if (pageURL === 'https://www.targetwebsite.com/feed/') {
      // 验证成功,清理会话
      sessionManager.delete(sessionId);
      await getCookies(page);
      return { status: 'success', info: 'Login Completed' };
    } else {
      throw new Error('Pin code verification failed');
    }
  } catch (error) {
    sessionManager.delete(sessionId);
    await browser.close();
    throw error;
  }
}

额外注意事项

  • 内存管理:一定要在会话完成(登录成功/失败)或浏览器断开时,从sessionManager中删除对应的会话并关闭浏览器,防止内存泄漏。
  • 会话过期:可以给会话设置过期时间(比如30分钟),定期清理无效会话,避免资源浪费。
  • 错误处理:在handleIpProcess中先检查会话是否存在,避免用户传入无效ID导致报错。

这样修改后,每个用户的自动化流程都有独立的会话ID,提交验证码时只要带上这个ID,就能精准找到对应的Puppeteer实例,不会再出现串号的问题了。

内容的提问来源于stack exchange,提问作者Noman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 22:07:37