把每日成长任务做成流水线:CDP 驱动 + 幂等判断 + 内容质量兜底

举报
yd_282927746 发表于 2026/11/04 08:28:43 2026/11/04
【摘要】 JS click 点了没反应、按钮其实是内层 span、脚本返回"提交成功"但没生效、卡片状态延迟十几小时——这些坑和"点不到按钮"无关,和"怎么判断状态才是真的"有关。附一套 CDP 流水线的四个设计原则和踩坑清单。

起因

成长中心每天有 5–6 个任务:签到、发文章、体验一下 AI 开发空间、提一条建议,每周还有 Star、Issue、实验、活动。单个任务几十秒,但每天重复七八次"打开页面 → 找卡片 → 点按钮 → 等跳转 → 确认状态",加起来是每天二十多分钟,而且是纯粹的机械劳动。

上个月我把它做成了一条 CDP 流水线。现在每天早晨跑一遍,除了需要人判断的地方(登录、选活动、确认发布),其余全自动。这篇文章拆解这套东西的设计,以及踩过的坑——大部分坑和"点不到按钮"无关,和"判断哪个状态才是真的"有关。

架构:一个持久 Chrome + 一组短脚本

核心只有三样:

  1. 一个带持久 profile 的 Chrome,用 --remote-debugging-port 启动,profile 单独放一个目录(Tools/hc-cdp)。好处是登录态、Cookie、扩展都在,关掉再开仍然免登录;
  2. CDP 直连(websocket-client 连 ws://127.0.0.1:9222/devtools/page/xxx),不依赖 Playwright/Puppeteer,脚本只有几十行;
  3. 一个脚本一个任务:grow_signin.py、publish_blog.py、suggestion_submit.py……每个都能单独跑、单独 dry-run。

选 CDP 而不是 Selenium,主要是因为它能直接把浏览器当"人"用:真实鼠标事件、键盘事件、多标签页切换、Page.navigate 都在同一个协议里;而 Selenium 的 window 管理在持久 profile 场景下经常打架。

设计一:先判断状态,再决定动作

所有任务脚本的第一件事不是点按钮,是读卡片状态:

// 卡片是否已完成
[].slice.call(document.querySelectorAll('div.cardItem'))
  .map(c => ({t: String(c.innerText).slice(0,20), done: /已完成/.test(c.innerText)}))

返回结构里直接带 done,脚本据此决定是执行还是跳过。这一步的价值在于可重入:中途失败重跑不会重复提交,也不会因为"今天已经做过"而报错。

再配一层兜底:签到这类有明确 UI 状态的任务(按钮文案从「立即签到」变「已签到」),以按钮状态为准,而不是以"我点过了"为准。

设计二:真实鼠标事件,不是 JS click

这是踩得最深的一个坑。很多人(包括我)第一版都是:

document.querySelector('button').click()   // 没用

有些组件库(Element UI 这类)把事件绑在特定层,或者用 mousedown/mouseup 组合判断、或者要求 isTrusted。表现是:JS click 返回正常,页面毫无反应,也不报错。

可靠做法是走 CDP 的输入事件:

for t in ("mouseMoved", "mousePressed", "mouseReleased"):
    kw = {"type": t, "x": x, "y": y}
    if t != "mouseMoved":
        kw.update(button="left", clickCount=1)
    ws.send("Input.dispatchMouseEvent", **kw)

坐标要先 getBoundingClientRect() 拿,再 scrollIntoView({block:'center'}),否则元素在视口外坐标就是负的。

还有一个进阶版:找对元素。页面上的「开始实验」可能是这样:

<div class="test-begin-right">
  <button class="el-button el-button--primary"><span>开始实验</span></button>
</div>

如果你按文本 === '开始实验' 去找,querySelectorAll('*') 先命中的是内层 <span> 或外层 <div>——点它们都不生效,必须点到 button.el-button 上。我的改法是按结构定位:

[].slice.call(document.querySelectorAll('button.el-button'))
  .filter(b => /开始实验/.test(b.innerText))[0]

设计三:先想清楚"凭证在哪一侧"

这条不是技巧,是判断标准,也是我卡了一周才想明白的。

有些操作靠登录 Cookie 就能做(签到、发文、提建议);有些操作背后连着云资源,要 STS 临时凭证,而凭证是由运行环境注入的——比如 AI 开发者空间的容器会给工作目录塞一个 sts-creds.json。这类操作在浏览器里怎么模拟都拿不到凭证,只能交给跑在容器里的 Agent 去做。

判断方法:看官方有没有提供 CLI 或 Skill。给了 npx skills add ... 这种入口的,八成是希望你在容器/终端里跑,而不是在网页上点。

设计四:不信任脚本自己的返回值

这条是被坑出来的。脚本点完提交,返回:

{"ok": true, "said": "提交成功 感谢您提交的建议"}

看起来成了。但有一次实际没提交成功——页面上的成功提示是历史遗留的 DOM,脚本读到就以为成了。所以我加了一条硬规则:任何写操作都要回读。

  • 提建议 → 打开「我的建议」,看总数有没有 +1;
  • 发文章 → 看积分明细有没有 +60;
  • 发作品 → 打开作品详情页,核对名称/作者/活动/仓库;
  • 签到 → 看积分明细里当天有没有「签到 +10」。

回读比任何返回码都可靠。这条规则也直接影响了我对"任务完成"的判断:成长中心的卡片状态是延迟的(实验任务要等 1–2 小时),所以判断依据是平台记录,不是 UI 上的那颗对勾。

踩过的坑(可以直接抄的清单)

现象 原因 处理
JS click 无反应 组件认 isTrusted 或绑在别的层 用 Input.dispatchMouseEvent
点了没反应、也没报错 点到内层 span 而不是 button 按 button.el-button 结构定位
元素坐标是负数 元素在视口外 先 scrollIntoView 再取坐标
页面布局跟你看到的不一样 视口宽度不同 先 Emulation.setDeviceMetricsOverride 固定宽度
上一轮的表格结构对不上 缓存了旧页面的 DOM 每次动作前重新 navigate
提交"成功"但没生效 读到了历史提示 DOM 回读列表/积分明细
换设备后脚本全挂 venv 路径变了 启动脚本里动态查找解释器

内容还是得人来

流程能自动化,内容不能。我给自己定了三条:

  1. 文章必须有可复现的东西。不是搬运官方文档,而是"我做了什么、报错原文是什么、最后怎么解决的"。像今天这篇,数据全部来自这几天真实跑出来的积分明细和时间戳。
  2. Issue 要先读代码。有一次给一个开源项目提改进,标题写"输出目录不存在会崩"——为了确认这一点,我把仓库 clone 下来读了 planner.py,确认 export_result() 里没有 makedirs,而且 README 的示例路径正好会踩到。这种 issue 才有价值。
  3. 建议要能落地。三段式:场景(怎么复现)、期望(改成什么样)、影响(谁受影响)。写"希望优化一下"的建议,一周后被归到"打回"是必然的。

结果与边界

一周下来,任务侧的机械劳动从每天二十多分钟降到几分钟,积分从 0 到 1400+。但更重要的收获是那几条判断标准:状态要回读、元素要定位到真按钮、凭证要看在哪一侧——这三条不只适用于这个平台,换成别的后台系统一样成立。

最后说清楚边界:这套东西只处理"重复的点击",所有需要判断的地方仍然是人——今天选哪个投稿活动、文章写什么、建议提什么问题,脚本一概不管。把机械劳动去掉,是为了把时间留给这些。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。