跨站任务判定的三层状态不同步:headless 自动化里的 external_check 陷阱

举报
yd_233360489 发表于 2026/11/11 12:34:14 2026/11/11
【摘要】 动作做完、按钮也变 Starred,积分却没到账?拆解跨站任务里外部平台、前端卡片、服务端积分明细三者的最终一致性问题,以及 visibilitychange 回调在无头浏览器里不触发、last_star_at 滞后一天、同站两个页面按钮状态不一致、明细弹窗缓存四个真实坑,给出以明细为唯一判据的对账规则。

现象:动作做完了,页面也说成功了,积分却没到

有个每天跑一次的浏览器自动化脚本,其中一项任务是「去第三方代码托管平台 Star 一个指定组织下的仓库」。脚本按官方链路走完:从任务中心点「去完成」跳到实践中心,再点「前往 Star 案例仓」进组织项目列表,挑一个没收藏过的项目点 Star。按钮当场从 Star 变成 Starred,刷新页面还是 Starred。

但回到成长中心,任务卡片依旧显示「去完成」,积分一分没涨。

第一反应通常是「是不是没走官方链路」「是不是 Star 被回滚了」。这两天的排查结论是:动作确实成功了,只是「成功」这件事分散在三个互相不同步的状态源里,而自动化脚本最容易被其中某一个骗到。

先拆清楚:一次跨站任务有三个状态源

这类任务的判定链路和纯站内点击不一样,它跨了两个域:

层 状态载体 更新时机 可信度
① 外部平台 代码托管服务端的 star 记录 点击即落库 最高,但脚本通常读不到
② 业务站前端 任务卡「已完成 / 去完成」、按钮文案 依赖接口回包 + 本地渲染 会被乐观更新和缓存污染
③ 业务站服务端 积分明细里的一笔进项 定时轮询外部平台后补发 最终判据(钱到账才算数)

三者之间都是最终一致,不是强一致。脚本如果拿 ② 当判据,就会在还没同步时误判「没做」而重复提交;如果拿 ① 当判据,又会因为读不到而干脆放弃核对。

坑一:headless 下 visibilitychange 回调根本不跑

翻官方前端 bundle 才发现,实践中心不是在你点 Star 的瞬间上报的,而是有一段定时校验:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    // 每 60s POST 一次服务端校验
    checkExternalTask("gitcode-star");
  }
});

也就是说,判定动作由「标签页重新可见」驱动,间隔约 60 秒。而在 Playwright / CDP 驱动的无头浏览器里,多标签页场景下只有当前 active 的那个是 visible,其余一直是 hidden;再加上脚本本身不会真的把标签切来切去,这个回调可能一次都不触发。

结果就是:Star 做完了,业务站服务端压根没被通知去查。前端卡片自然一直「去完成」。

解法不是伪造数据,而是按官方自己的参数补一次校验。从 bundle 里能定位到上报接口与 body:

await fetch("/api/growth/tasks/gitcode-star/submit", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ case_id: null, payload: { action: "check" } })
});

注意 action: "check" 和外层 {case_id, payload} 结构必须从前端代码里抄,猜一个 {} 过去只会拿到参数错误。这属于「替页面补触发它本该自己触发的调用」,不是绕过校验。

坑二:服务端的 last_star_at 会滞后一整天

手动补了校验,回包长这样:

{
  "task_key": "gitcode-star",
  "period_key": "2026-11-11",
  "completed": false,
  "progress": {
    "today": "2026-11-11",
    "need_daily_star": true,
    "last_star_at": "2026-11-10T10:25:55+08:00",
    "matched": [],
    "missing": ["org:xxx"]
  }
}

last_star_at 停在昨天,说明业务站服务端缓存的是上一次轮询到的外部平台快照,今天这次 Star 还没同步过来。这不是判定失败,是同步窗口没到。

这里最容易犯的错是「再 Star 一次」。任务规则明确要求每天 Star 不同项目且不可取消,重复点同一个项目不产生新的有效行为,反而把状态弄脏。正确做法是把这次判定标记为「待同步」,退出本次流程,隔天用积分明细复核——实测这笔 +30 会在后续轮询里自动补发,明细里能看到带时间戳的进项。

所以这类任务的正确判据是积分明细,不是任务卡片:

// 只认明细,卡片状态只作参考
const rows = await readPointDetails();   // 每笔带时间 + 来源 + 积分
const done = rows.some(r =>
  r.date === today && /Star.*代码仓/.test(r.source));
if (!done) markPending("awaiting_external_sync");  // 不重做

坑三:同一个站点,两个页面的按钮状态不一致

另一个更隐蔽的坑出现在选项目环节。组织主页的精选项目卡片上,所有仓库的按钮都显示 Star;但切到项目标签页,同样的仓库里已经有好几个是 Starred。

// 错误:在组织主页上找第一个 "Star" 按钮
const btn = [...document.querySelectorAll(".status-btn")]
  .find(b => b.innerText.trim() === "Star");   // 可能命中已收藏的仓库

// 正确:切到权威列表页,按仓库名精确归属再判断
const card = [...document.querySelectorAll("li,div[class*=card]")]
  .find(c => c.innerText.includes(repoName));
const target = [...card.querySelectorAll(".status-btn")]
  .find(b => b.innerText.trim() === "Star");
target.click();

原因是主页卡片是「精选项目」推荐位,按钮只渲染默认态,不回读当前用户的收藏关系;项目列表页才会带真实状态。自动化脚本必须先确认自己在哪个数据源上,再决定读哪个字段。凭 class 名相同就假定语义相同,是这类脚本的高频翻车点。

另外,外部平台的开放接口也不总是可用的兜底:试过直接查 /api/v5/user/starred,返回 400 要求 private-token 头。没有 token 时,页面按钮状态反而比 API 更可靠——前提是用对页面。

坑四:积分明细弹窗本身有缓存

即便切回成长中心,直接点开积分明细也可能看到旧数据。弹窗是前端缓存,不刷新页面不会重新拉。多次实测下来,稳定流程是:

  1. switch 回成长中心标签;
  2. 先 reload,等页面重新拿到登录态与积分;
  3. 再点 div.user-message-view-textButton-item-point 开明细;
  4. 用正则 /(\d+)\s*我的积分/ 读当前积分(反着写会误读「已连续签到」附近的数字)。

漏掉第 2 步,会一直看着旧余额得出「没到账」的结论,然后触发一轮本不该有的重试。

把结论收敛成几条规则

规则 为什么
跨站任务以积分明细为唯一终态判据 卡片与按钮都会滞后或乐观更新
动作与核验解耦,核验可补触发 页面可见性回调在 headless 下不可靠
补触发必须抄官方参数,不构造业务数据 既避免绕过校验,也不污染账号状态
判定为「待同步」时绝不重做 不可逆动作(Star / 提交作品 / 发文)重复会消耗配额
读取前先确认数据源页面 同站不同视图的字段语义可能不同
对账前强制 reload 明细弹窗有前端缓存

本质上,自动化脚本面对的不是一个「做完就返回成功」的同步世界,而是一组带不同延迟的状态机。把「动作成功」和「判定入账」当成同一件事,是所有重复提交和误判的起点。写成显式的 done / pending / failed 三态,再配一份带时间戳的明细对账,比在 UI 上反复刷「去完成」有用得多。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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