LuckyHYP
返回文章归档

用 AdsPower 的 Local API 和它自带的 2Captcha 插件,把评论外链的验证码站也跑成全自动

用 AdsPower 的 Local API 和它自带的 2Captcha 插件,把评论外链的验证码站也跑成全自动

Sep 17, 2026

用 AdsPower 的 Local API 和它自带的 2Captcha 插件,把评论外链的验证码站也跑成全自动

3a15948f5b180d7faf4f0cdd

王焱

贵阳的时候从小龙哥那边学来了一个神器 AdsPower,然后把它融入到了我的工作流里面,让它去处理那些我原本自动化投放失败的地址。

做WordPress 评论外链,脚本批量投放这件事本身不难,难的是最后一公里:目标站开着 reCAPTCHA、hCaptcha、Turnstile 或者CleanTalk 这类墙,脚本一撞就停。我这一批目标里撞墙的站占了两成左右,以前的处理方式是攒着,等有空了人工去点。这篇讲我怎么把这两成也跑成无人值守,中间AdsPower起了什么作用,以及它的2Captcha插件真正用起来和宣传的差在哪。

代码是 Node + playwright-core,不依赖本机Chrome。

整条流水线长什么样

先把上下文交代清楚,不然验证码这一段没法单独理解。

候选站(HTTP 筛查过,确认是 WP、有评论表单)
  │
  ├─ 自动提交器(headless):导航 → 找表单 → LLM 写评论 → 逐字填 → 提交 → 判结果
  │     运行时检出验证码墙 → 不提交,记 needs_human:<墙类型>
  │
  └─ 收尾通道(有头,本文主角):只吃 needs_human 的站
        人工模式:机器填好一切,人过验证码 + 点提交
        AUTO 模式:2Captcha 插件解题,脚本点提交,无人值守
  │
  └─ 结果账本 results.jsonl(append-only,一行一个判定)
        几天后复查进审核的站,定终态

两条通道之间只共享那个 JSONL文件.每个「目标域 + 产品」取最新一行判定,最新判定是 needs_human:* 的进收尾队列。

AdsPower 在里面干三件事

我最初的自动提交器是本机 headless Chrome加一堆反检测补丁,人工收尾用本机有头Chrome。后来两条都换成AdsPower,原因是它同时解决了三个我不想自己维护的问题:

指纹. UA、时区、语言、canvas、WebGL 这些由 profile 自管,脚本不用再背 stealth 定制件,也不用跟Chrome版本升级赛跑。

有头窗。 人工收尾需要一个真人能直接操作的窗口。AdsPower 起的 profile天生有头,脚本通过CDP 接进去,人和脚本操作的是同一个页面,不需要「脚本先做一半,人再另开浏览器接着做」这种断层。

扩展。 profile 里能装 Chrome扩展,AdsPower 的扩展中心直接有2Captcha 官方插件。装进去之后它会在页面上每个验证码控件旁注入一个 Solve按钮,脚本在 DOM里能看到、能点。全自动就是从这里来的.

接入:Local API起 profile,CDP 接进去

AdsPower 客户端开着,Local API跑在本机 50325端口。启动层就三步:调 API 起 profile,拿返回的 WebSocket端点,connectOverCDP 接上。

const { chromium } = require('playwright-core');

async function launch({ proxyUrl, headless = true, profile } = {}) {
  const c = cfg(profile);              // config.json 的 adspower 段:api / key / user_id
  await setProxy(c, proxyUrl);         // 代理要在 start 之前写进 profile
  const pre = await apiCall(c, 'GET', \`/api/v1/browser/active?user_id=${c.userId}\`);
  if (pre?.data?.status === 'Active') await stopAndWait(c);   // 上次的孤儿实例先清掉

  const r = await apiCall(c, 'GET',
    \`/api/v1/browser/start?user_id=${c.userId}&headless=${headless ? 1 : 0}\`
    + \`&disable_password_filling=1&open_tabs=1&ip_tab=0\`);
  const browser = await chromium.connectOverCDP(r.data.ws.puppeteer);
  const close = async () => { await browser.close().catch(() => {}); await stopAndWait(c); };
  return { browser, ctx: browser.contexts()[0], close };
}

自动提交器和收尾通道用的是同一个 launch(),差别只在 headless 和 profile名。这段代码现在看着平淡,里面每个参数都是烧了一波站才加上的.

stop 是异步的。 /browser/stop 返回 success 之后,浏览器进程还要几秒才真的死。串行跑的时候下一站紧接着start,撞上的是一个 CDP端口还开着但不会应答的壳,connectOverCDP 会卡满 30 秒超时。文档没提这一点。修法是 stop 之后轮询 /browser/active,状态不是 Active了才返回:

async function stopAndWait(c, timeoutMs = 15000) {
  await apiCall(c, 'GET', \`/api/v1/browser/stop?user_id=${c.userId}\`).catch(() => {});
  const t0 = Date.now();
  while (Date.now() - t0 < timeoutMs) {
    const a = await apiCall(c, 'GET', \`/api/v1/browser/active?user_id=${c.userId}\`).catch(() => null);
    if (a?.code === 0 && a.data?.status !== 'Active') return;
    await new Promise(r => setTimeout(r, 1000));
  }
}

exit 抢在close 前面,profile 就一直 Active。 提交器判完结果直接 process.exit,finally 里的close永远跑不到,下一站start报 [user_id] is being used 。我一波49个站全记成了失败,回头看账本才发现是自己锁死了自己。修法是先关浏览器再 exit,并且这类基建错误一律退回选池,不让目标站背锅。

用start返回的 ws.puppeteer 直连。 不要拿 http://127.0.0.1:端口 让playwright 自己协商,浏览器状态差的时候协商会挂30秒。官方脚本样例就是直连,照做。

open_tabs=1&ip_tab=0 。 默认每次启动会开一张 IP检测页,走代理白烧流量,还会恢复上次的历史标签。

频率限制不报限流。 profile数在200以下时全局2 次/秒,start/stop类1 次/秒。超了返回的是 The server is not working well 之类的通用错,很容易当成客户端挂了去重启。我在 apiCall 里加了间隔阀,start/stop 至少隔1.1 秒,其余 0.55 秒。

代理只能在start 之前写进profile。 CDP接进去之后 playwright 侧设不了代理,走 /api/v1/user/update 写 user_proxy_config 。脚本给了 --proxy 就写,没给就不动profile 原配置。

语言时区默认跟 IP。 出口在日本,navigator.language 就是 ja.要钉英语必须 language_switch: '0',只传 language 数组没用。

同一个 profile 绝不并发。 启动层分不清「别的任务正在用」和「上次失败的孤儿」,一律stop 清场。要并发就每路一个独立 profile。

指纹选错,投达率直接腰斩

接上 AdsPower之后第一轮跑出来的数据比本机 Chrome还差,我一度以为是内核被识别了。做了一组A/B,同一批目标站、同一代理出口、严格串行:

| 浏览器 | 站数 | 投达率(成功 + 进审核 + 撞墙) | | --- | --- | --- | | 本机 headless Chrome | 550 | 26.2% | | AdsPower,iPhone移动指纹 | 513 | 10.1% | | AdsPower,Mac 桌面指纹 | 50 | 40% |

移动指纹臂十轮全输,还独有几十例「执行上下文丢失」和「落地找不到评论区」的异常。原因很土:WP评论表单是桌面形态,拿手机指纹去访问,响应式布局把评论区折叠,脚本找不到控件,站方看到的也是一个别扭的访客。换成桌面指纹,投达率是本机Chrome 的一倍半。桌面组只有50站,精确数还要复测,方向是清楚的:profile 的设备形态要和目标页面形态一致,「AdsPower 效果差」多半是profile建错了,不是软件的问题。

人工模式:机器填好一切,人只碰验证码

收尾通道默认是人工模式。机器开有头profile,导航到文章页,找评论表单,LLM 读标题和首段写一条评论,逐字敲进去(名字、邮箱、正文),蜜罐字段留空,该勾的隐私协议勾上,默认勾着的newsletter退掉。然后在页面右下角挂一个浮条:「我已提交 / 重填 / 跳过 / 退出」,还有挪角和收起两个小按钮,因为 reCAPTCHA的挑战从顶部弹、有的站控件在右下角,浮条会挡。

人过验证码,点站上自己的提交按钮。机器盯 URL,一变就判结果、落账、关页、开下一站.

浮条是 pg.evaluate 注进去的普通 DOM,按钮点击只写一个 window.__wpbAction.机器侧一个循环读三种信号:

async function waitEvent(pg, startUrl) {
  while (true) {
    if (pg.isClosed()) return { closed: true };
    const st = await Promise.race([
      pg.evaluate((id) => ({ act: window.__wpbAction || null, bar: !!document.getElementById(id) }), BAR_ID),
      new Promise((r) => setTimeout(r, 3000, undefined)),   // 站点弹原生 alert 时 evaluate 会挂住
    ]).catch(() => undefined);
    if (st?.act) return { action: st.act };                // 人按了按钮
    if (pg.url() !== startUrl) return { landed: pg.url() }; // 跳转 = 提交了
    if (st && !st.bar) return { reloaded: true };          // 浮条没了 = 整页 reload 了
    await pg.waitForTimeout(1200).catch(() => {});
  }
}

第三个信号是踩出来的。过验证码经常触发整页 reload,填好的正文被清,注入的浮条也一起没了。所以浮条消失本身就是reload 信号,机器检测到就重挂浮条、重填正文,人什么都不用管。我第一版把「重填吗?」这类追问放在终端里,人盯着浏览器根本看不见,两边互等,一晚上白过。所有追问后来全搬到页面上,标签页被人关了就开一张空白页把问题摆上去。

人工模式一小时能过 20到 30 个站。但这依然要人坐在那儿,所以有了下面这一段。

AUTO模式:2Captcha 插件解题,脚本负责其余

AdsPower扩展中心里的2Captcha 插件,装进收尾用的 profile,填 API key,开reCAPTCHA / hCaptcha / Turnstile。它的工作方式是在每个验证码控件旁注入一个 .captcha-solver 按钮,点了发解题请求,解完把token写进 g-recaptcha-response 之类的隐藏字段。插件自己有 autoSolve 和 autoSubmit两个开关,理论上全开就是全自动。实际接下来三个问题,每个都得脚本兜.

配置改了不生效。 插件选项页没有保存按钮,勾选框靠合成 click 事件不触发持久化。要改只能在扩展页里直接写 chrome.storage.local 的 config 对象。不知道这一条的话会以为插件坏了。

全局 autoSolve 是烧钱漏洞。 它对页面上每一个验证码控件都发一次解题。一个普通 WP 站,评论区一个、侧栏订阅框一个、页脚联系表单一个,我实测过一页四个控件四次解题,autoSubmit还把无关表单也提交了。正确做法是全局autoSolve 全关,脚本只点评论表单那一个Solve按钮。找法是从评论表单往 DOM上游走六层,找第一个 .captcha-solver;全页只有一个控件时直接点它:

const clicked = await pg.evaluate(() => {
  const form = document.querySelector('#commentform, form[action*="comment" i]');
  let scope = form, btn = null;
  for (let i = 0; i < 6 && scope && !btn; i++) {
    btn = scope.querySelector('.captcha-solver[data-state="ready"]') || scope.querySelector('.captcha-solver');
    scope = scope.parentElement;
  }
  if (!btn) {
    const all = [...document.querySelectorAll('.captcha-solver')];
    if (all.length === 1) btn = all[0];
  }
  if (btn && btn.dataset.state !== 'solving' && btn.dataset.state !== 'solved') { btn.click(); return true; }
  return false;
});

Solve按钮可能晚于表单填完才出现,所以重试五次、每次隔三秒。

插件的自动提交是 form.submit() 。 这会绕过站上绑定的 onsubmit 和 ajax 逻辑,不少站token 填了、页面纹丝不动,等于没提交.修法是脚本盯 token字段,一填入就自己去点真正的提交按钮,和人的手势一致;已经跳转的站说明插件提交成功了,不补:

const tokSel = 'textarea[name="g-recaptcha-response"], textarea[name="cf-turnstile-response"], '
             + 'input[name="cf-turnstile-response"], textarea[name="h-captcha-response"]';
let solved = false;
const t0 = Date.now();
while (Date.now() - t0 < 120000) {              // token 有效期 120 秒,过了再点也没用
  if (pg.isClosed() || pg.url() !== startUrl) break;
  solved = await pg.evaluate((sel) =>
    [...document.querySelectorAll(sel)].some((el) => el.value && el.value.length > 20), tokSel);
  if (solved) break;
  await pg.waitForTimeout(2000);
}
if (solved && pg.url() === startUrl) {
  await pg.evaluate(() => {
    const form = document.querySelector('#commentform, form[action*="comment" i]');
    const btn = form?.querySelector('button[type=submit], input[type=submit], #submit, input[name=submit], button');
    btn?.click();
  });
}

还有一个设置要开:插件的 useProxy,让解题请求走和浏览器同一个出口IP。reCAPTCHA的 token对 IP 一致性敏感,解题用的 IP 和提交用的 IP不一样会被拒。

AUTO模式下人工模式的那些追问全部不问。找不到表单直接记失败;解了题、提交了、落地页面什么特征都没有,记一行 auto_attempt,判定不变,留在队列给真人。等插件解题加提交的总宽限 180 秒,到点就地读页面判.

花钱的通道要有守卫

reCAPTCHA 解一次约 $0.003,100个站清一遍三毛钱,不贵。贵的是没有守卫时被死墙无限吃掉的那部分.三条规则:

  • 开关严格解析。只有 WPB_HUMAN_AUTO=1/true/yes 开,0 、 false 、乱写的值打印警告按关闭处理。
  • 每站自动尝试一次封顶,撞墙累计四次停试。失败多为站方本来就不放行,再试是白烧。
  • 双墙站跳过。极个别站提交后再弹第二层自定义验证码,记 needs_human:captcha_other 留在队列,下一趟又解一次又撞第二层,无限循环.AUTO 看到「上一趟已经处理过 + 这一趟还是 captcha_other」直接跳过,攒一批交给人。

定时清队:队列是文件,cron就够

撞墙站是自动提交器慢慢攒出来的,收尾通道每小时点一次火就够,不需要常驻。包装脚本三件事:

pgrep -f "node wp_bypass_human.js" >/dev/null 2>&1 && exit 0     # 有实例在跑就退出

for H in ${REMOTE_HOSTS:-}; do                                    # 多机:分机账本回流合并
  scp -o BatchMode=yes "$H:$REMOTE_RESULTS_PATH" "run/remote_pull/r_$H.jsonl" \
    && python3 scripts/wpb_merge_results.py "run/remote_pull/r_$H.jsonl"
done

WPB_HUMAN_AUTO=1 node node-tools/wp_bypass_human.js >> logs/human_auto_cron.log 2>&1

多机的时候,分机跑自动提交器攒出来的撞墙行要先合并进本机账本,本机队列才看得见。合并按行去重,append-only,反复跑不会重。macOS 用 launchd 一样,注意它环境干净,node 和 python3 写绝对路径,key 放 config.json.

判定口径:两条通道一个函数

自动提交器和收尾通道共用同一个落地判定函数,成功的定义是两条都满足:本次评论正文出现在页面上,并且指向目标域的链接在本次评论的容器里,rel 不含 nofollow、ugc、sponsored。只看「页面上有没有指向目标域的链接」不行,侧栏的旧链接、别人评论里的链接都会造成假成功。评论在页但容器里没链接,记进审核,几天后复查再定。

判定放在同一个函数里的意义在于,两条通道的成功率才能放在一张表上比。我最早两边各写一份,人工通道的口径松,数据看着很好,合并之后才发现松的那部分全是假成功。

几句总结

AdsPower 在这套东西里不是「更高级的浏览器」,它是三个基础设施的合集:指纹、有头窗、扩展宿主。Local API的坑集中在生命周期(stop异步、exit顺序、频率限制),摸清之后很稳。2Captcha 插件能用,但它的 autoSolve 和autoSubmit两个开关都不能开,解题触发和提交动作要收回到脚本手里,不然一边烧钱一边漏提交.

至于指纹,桌面表单配桌面指纹,这一条比所有反检测技巧加起来都值钱。

new.web.cafe

Web.Cafe


评论

0

评论提交后需要审核。