做多平台分发这两年,我踩过最贵的坑不是接口 500,也不是选择器失效。
是它告诉我发成功了,我信了,三天后才发现后台什么都没有。
这篇把三次真实事故拆开,给出一套「先验证、再提交」的发布器写法。代码用 TypeScript,思路和语言无关。
事故一:按钮文案变了,请求根本没发
某平台的发布按钮,用 element.click() 触发后,文案会立刻变成「发布中...」,几十秒后又自己变回来。没有 toast,没有报错,控制台干净。
当时脚本的判断逻辑是「按钮文案变化 = 提交成功」,于是继续往下走去列表页找新文章------当然找不到。
后来在 Network 里看,整个过程没有一条提交请求,只有埋点。
修复很土:换成 CDP 的可信点击(Input.dispatchMouseEvent,或 Playwright 的 locator.click()),一次就过了。
结论:按钮反馈只是乐观更新,不能当提交成功的判据。真正能信的只有两样,真实请求和后台数据。
事故二:标题被导入文件名覆盖了
第二个更隐蔽。Markdown 导入后,标题自动填成文件名 xxx_20261017。我用原生 value setter 改了标题,紧接着在同一次调用里点了发布。
结果:成功页显示「发布成功」,列表里也确实有这条,但从另一个入口(流量券的可推广文章列表)看过去,标题还是文件名。
原因是平台在打开发布弹窗时会把标题写进内部 model,最后一次赋值离弹窗太近,model 还没 flush。
修复两步:改完字段等 3~4 秒;开弹窗单独一次调用。发布后必须去列表复核字段,不信成功页。
事故三:创作后台和主站是两个账号
这个跟代码质量无关,纯粹是环境。
某平台的创作服务平台和主站登录态是分开的。后台登着 A 账号,主站登着 B 账号。发布一切正常,但去主站点赞同评论一律被拦,提示还挺误导人。
后来固定加了一条前置校验:发布前读一次页面上的账号名,和目标账号比对,不一致直接终止,不去猜。
多账号环境下,这条比任何重试都有用。
设计:verify-first 发布器
三次事故指向同一个设计缺陷:把「动作完成」当成了「结果发生」。
改成 verify-first 之后,每个写操作都必须配一个只读探针:
ts
type Probe = () => Promise<{ found: boolean; detail?: unknown }>;
async function publishWithProbe(action: () => Promise<void>, probe: Probe, tries = 3) {
await action();
for (let i = 0; i < tries; i++) {
const state = await probe();
if (state.found) return { ok: true, state };
await sleep(1500);
}
return { ok: false, stage: "verify" };
}
两条约束建议直接写进注释,不然下次改代码的人一定会破坏它:
- probe 只能只读。写操作套写操作,出问题时根本分不清哪一步坏的。
- 校验失败只重试读,不重试写。重复提交的代价远大于少发一篇。
配合的三个探针:
- 后台列表探针:按发布时间倒序,看标题 + 状态 + 时间三个字段
- 请求探针:page.waitForResponse 抓目标域名的 POST,超时即失败
- 换视角探针:用未登录态打开详情页,或从另一个列表入口查
三个都命中才算成功。只命中一个,按失败处理。
提交前的 8 条硬检查
这套东西真正的价值不在发布,在于让脚本学会「不做」。
- 账号名正确(多账号必查)
- 页面无验证码、无实名弹窗
- 正文无联系方式、外链、二维码
- 无承诺词与极限词
- AI 辅助内容已勾选平台声明
- 本账号 2 小时内无发布
- 今天没用过同一模板骨架
- 目标内容状态允许提交(审核未通过的不重复提交)
任何一条不过,直接终止并记录,不进入发布流程。
最后
自动化省下来的时间,很容易被「错误的成功」吃掉,而且吃得悄无声息。
宁可多发一次查询,也别少一次回读。后台列表不会说谎。