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

使用Puppeteer与Puppeteer-Cluster出现“Attempted to use detached Frame”错误

问题描述

基于Express.js构建应用,使用Puppeteer和puppeteer-cluster访问乌克兰测试平台自动完成测试,流程为:

  • 进入用户控制台
  • 搜索测试
  • 进入测试
  • 完成测试

但在进入测试并回答若干问题后,出现错误:Attempted to use detached Frame 'D78184744F4CA70F50307B66DDF53250',提示无法获取类为.v-test-questions-title的元素(该元素实际存在)。已尝试修改Puppeteer配置、容器运行、更换浏览器、调整代码、添加等待导航/网络空闲等方法,均未解决,仅部分方案改变错误名称。注意:回答问题时URL不会变化。


相关代码

solveSingleAnswerQuestion方法

async solveSingleAnswerQuestion() {
        const pageElement = new PageElement(this.page)
        const question = await pageElement.getContent('.v-test-questions-title') // 错误发生在此处

        const variants = await pageElement.getContents(
            '.v-test-questions-radio-block',
            '.v-test-questions-checkbox-block'
        )
        console.log({ question, variants })
        const answerIndex = await generateResponseIndex(
            question || '',
            variants
        )
        console.log({ answerIndex })

        const variantsElements = await pageElement.getElements(
            '.v-test-questions-radio-block',
            '.v-test-questions-checkbox-block'
        )
        await variantsElements[answerIndex - 1].click()

        const submitButton = await pageElement.getElement('.v-blue-button-test')

        await submitButton.click()

        await waitFor(5000)
    }

PageElement类

import { cleanString, waitFor } from '@/utils'
import type { ElementHandle, Page } from 'puppeteer'

export class PageElement {
    constructor(private readonly page: Page) {}

    async loadAllElements() {
        const pageElement = new PageElement(this.page)
        const button = await pageElement.getElement('.vo-btn-blue')

        if (button) {
            await button.click()
            await waitFor(5000)

            await this.loadAllElements()
        }
    }

    async getElementsByUniqueSelector(selector: string, fallback?: string) {
        const elements = await this.getElements(selector, fallback)

        const filteredElements = (
            await Promise.all(elements.map(this.isElementWithSingleClass))
        ).filter((element) => element !== null)

        return filteredElements
    }

    async getContentsByUniqueSelector(selector: string, fallback?: string) {
        const elements = await this.getElementsByUniqueSelector(
            selector,
            fallback
        )

        const contents = await Promise.all(
            elements.map((element) => element.evaluate((el) => el.textContent))
        )

        return this.cleanContents(contents)
    }

    async getContents(selector: string, fallback?: string) {
        const elements = await this.getElements(selector, fallback)

        const contents = await Promise.all(
            elements.map((element) => element.evaluate((el) => el.textContent))
        )

        return this.cleanContents(contents)
    }

    async getElements(selector: string, fallback?: string) {
        const elements = await this.waitForAndGetElements(selector)

        if (elements.length > 0) {
            return elements
        }

        if (fallback) {
            return await this.waitForAndGetElements(fallback)
        }

        return []
    }

    async getContent(selector: string, fallback?: string) {
        const element = await this.getElement(selector, fallback)

        const content = await element?.evaluate((el) => el.textContent)
        const cleanedContent = cleanString(content || '')

        return cleanedContent
    }

    async getElement(selector: string, fallback?: string) {
        const elements = await this.getElements(selector, fallback)

        return elements[0]
    }

    private async waitForAndGetElements(selector: string) {
        const el = await this.page
            .waitForSelector(selector, {
                visible: true,
                timeout: 5000,
            })
            .catch(() => null)

        const elContent = await el?.evaluate((el) => el.textContent)
        console.log({ elContent })

        return await this.page.$$(selector) // 错误提示此处无法获取.v-test-questions-title元素
    }

    private async isElementWithSingleClass(element: ElementHandle) {
        const className = await element.getProperty('className')
        const classList = (await className.jsonValue()).toString().split(' ')

        return classList.length > 1 ? null : element
    }

    private async cleanContents(contents: (string | null)[]) {
        return contents.map((content) => cleanString(content || ''))
    }
}

集群初始化代码

import { Cluster } from 'puppeteer-cluster'

export const cluster = await Cluster.launch({
    concurrency: Cluster.CONCURRENCY_CONTEXT,
    maxConcurrency: 1000,
    // monitor: true,
    puppeteerOptions: {
        headless: true,
        dumpio: true,
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage',
        ],
    },
})

服务代码

async solveTestByTitle(testTitle: string) {
        await this.enterCabinet()

        const test = new Test(this.page)
        const pageElement = new PageElement(this.page)
        await test.startSolvingTestByTitle(testTitle)

        const question = await pageElement.getContent('.v-test-questions-title')
        console.log({ questionInService: question })

        while (question) {
            await test.solveSingleAnswerQuestion()
        }
    }

解决方向

  1. 处理Frame分离问题
    Frame分离说明当前页面上下文已失效,测试页面可能通过动态替换Frame或重渲染销毁了原关联上下文。可尝试:

    • 每次操作前通过page.mainFrame()获取最新主Frame,替换原操作上下文
    • 监听frameattached/framedetached事件,在Frame变化时重新绑定操作逻辑
  2. 优化元素等待逻辑
    当前waitForAndGetElements中两次操作(等待元素+获取元素)存在时间差,可能导致页面变化后获取失败。合并逻辑并增加重试:

    private async waitForAndGetElements(selector: string, retryCount = 2) {
        try {
            await this.page.waitForSelector(selector, { visible: true, timeout: 5000 });
            return await this.page.$$(selector);
        } catch (e) {
            if (retryCount > 0) {
                await waitFor(2000);
                return this.waitForAndGetElements(selector, retryCount - 1);
            }
            throw e;
        }
    }
    
  3. 修复循环逻辑漏洞
    原服务代码中question仅初始化一次,会导致无限循环。修改为每次循环重新检查:

    async solveTestByTitle(testTitle: string) {
        await this.enterCabinet()
    
        const test = new Test(this.page)
        const pageElement = new PageElement(this.page)
        await test.startSolvingTestByTitle(testTitle)
    
        while (true) {
            const question = await pageElement.getContent('.v-test-questions-title')
            if (!question) break;
            console.log({ questionInService: question })
            await test.solveSingleAnswerQuestion()
        }
    }
    
  4. 调整集群并发数
    当前maxConcurrency: 1000过高,易导致浏览器资源耗尽引发上下文异常。降低至10-50区间,观察问题是否缓解。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 17:32:04