把每日成长任务做成流水线:CDP 驱动 + 幂等判断 + 内容质量兜底
起因
成长中心每天有 5–6 个任务:签到、发文章、体验一下 AI 开发空间、提一条建议,每周还有 Star、Issue、实验、活动。单个任务几十秒,但每天重复七八次"打开页面 → 找卡片 → 点按钮 → 等跳转 → 确认状态",加起来是每天二十多分钟,而且是纯粹的机械劳动。
上个月我把它做成了一条 CDP 流水线。现在每天早晨跑一遍,除了需要人判断的地方(登录、选活动、确认发布),其余全自动。这篇文章拆解这套东西的设计,以及踩过的坑——大部分坑和"点不到按钮"无关,和"判断哪个状态才是真的"有关。
架构:一个持久 Chrome + 一组短脚本
核心只有三样:
- 一个带持久 profile 的 Chrome,用
--remote-debugging-port启动,profile 单独放一个目录(Tools/hc-cdp)。好处是登录态、Cookie、扩展都在,关掉再开仍然免登录; - CDP 直连(
websocket-client连ws://127.0.0.1:9222/devtools/page/xxx),不依赖 Playwright/Puppeteer,脚本只有几十行; - 一个脚本一个任务:
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 路径变了 | 启动脚本里动态查找解释器 |
内容还是得人来
流程能自动化,内容不能。我给自己定了三条:
- 文章必须有可复现的东西。不是搬运官方文档,而是"我做了什么、报错原文是什么、最后怎么解决的"。像今天这篇,数据全部来自这几天真实跑出来的积分明细和时间戳。
- Issue 要先读代码。有一次给一个开源项目提改进,标题写"输出目录不存在会崩"——为了确认这一点,我把仓库 clone 下来读了
planner.py,确认export_result()里没有makedirs,而且 README 的示例路径正好会踩到。这种 issue 才有价值。 - 建议要能落地。三段式:场景(怎么复现)、期望(改成什么样)、影响(谁受影响)。写"希望优化一下"的建议,一周后被归到"打回"是必然的。
结果与边界
一周下来,任务侧的机械劳动从每天二十多分钟降到几分钟,积分从 0 到 1400+。但更重要的收获是那几条判断标准:状态要回读、元素要定位到真按钮、凭证要看在哪一侧——这三条不只适用于这个平台,换成别的后台系统一样成立。
最后说清楚边界:这套东西只处理"重复的点击",所有需要判断的地方仍然是人——今天选哪个投稿活动、文章写什么、建议提什么问题,脚本一概不管。把机械劳动去掉,是为了把时间留给这些。
- 点赞
- 收藏
- 关注作者
评论(0)