有一次,我遇到一个很容易把人带进坑里的结果:视频上传之后,页面实际上已经显示作品发布成功,但负责回读结果的命令却超时退出,返回了失败。乍看之下,这像是一次没有完成的发布;如果只看命令最后的退出码,我大概会立刻重试。
但管理页里已经有那张作品卡,状态也已经变成了已发布。真正的问题不是“有没有再次点发布”,而是系统到底有没有读懂当前页面。旧验证器还在寻找已经变化的作品管理文案和卡片结构,找不到旧定位,就把“没找到”翻译成了“没发布”。
这件事让我重新意识到,自动化最危险的错误不一定是动作没有执行,而是动作已经执行,却被错误的验证结果覆盖了。前者通常会停在那里,后者却会推动调用方继续操作,甚至把同一个内容再次发出去。
页面说成功,命令为什么说失败
发布动作和发布结果经常被塞进同一个命令里:先上传文件,等待页面变化,再从页面上找一张卡,最后根据卡片文字返回成功或失败。这样的流程在页面稳定时很顺,但它把几个性质完全不同的问题绑在了一起。
上传是一个动作,页面回读是一次观察,已发布则是业务状态。动作发生过,不代表观察一定成功;观察没有命中,也不代表业务状态一定不存在。如果验证器把这3件事压成一个布尔值,任何一层的异常都会变成“发布失败”。
这次就是第2层出了问题。上传实际完成了,管理页也有作品,但旧验证器依赖的作品管理文案和卡片定位已经变化。它等待了很久,最后仍然返回退出码1。系统给出的“失败”,准确地描述了验证器没有找到目标,却没有准确描述发布动作本身。
“验证没有命中”不能直接等同于“业务动作没有发生”。
这个区别看起来像文字游戏,实际会改变下一步。若调用方收到失败就自动重试,它不会知道前一次可能已经成功。于是一个定位问题,会被放大成重复发布问题;一次页面改版,可能变成内容列表里多出几份相同作品。
最容易误判的是把退出码当事实
自动化系统喜欢用退出码表达结果,因为退出码简单、稳定,也方便上游判断。但退出码只是某个程序对自己这一轮工作的总结,不是外部世界的最终事实。负责上传的程序可以返回成功,负责验证的程序也可以因为找不到页面元素而返回失败;两者并不矛盾。
当时最容易走的错误捷径,就是看到验证超时,直接把整次发布判成失败并重试。这个选择很有诱惑力:不用理解页面,不用追查旧定位,也不用区分上传和回读,只要再跑一遍命令即可。短期看,它似乎提高了“成功率”;长期看,它把不确定性藏进了副作用更大的动作里。
如果不处理这个问题,后面会导致重复作品、重复通知,甚至让人无法判断哪一份才是最终版本。更麻烦的是,重复发布可能不是马上暴露。调用方也许先看到一个成功结果,过一段时间才发现列表里多出一张相同卡片,届时很难再把原因追溯到那次验证超时。
我选择先确认页面,再决定是否重试,是因为发布属于不可随意重复的动作。验证失败时,最稳妥的结果不是“失败”,而是“未知”或“待复核”。未知会让上游暂时停一下,失败却可能让上游放心地重新执行。对有副作用的操作来说,保守地停住,比自信地多做一次更安全。
先把3层状态拆开
我后来把流程按3层重新想了一遍。第1层是发布动作:文件是否提交,页面是否进入上传过程,动作是否收到明确的接受结果。第2层是页面回读:管理页是否能打开,当前页面有哪些文案,卡片是否能被定位。第3层是业务状态:作品究竟是已发布、审核中、未通过,还是暂时无法判断。
这3层之间应该有明确的边界。第1层成功,只能说明系统完成了提交;第2层成功,只能说明系统读到了页面;第3层才负责回答业务问题。如果第2层因为页面结构变化没有读到卡片,第3层就不应该凭空返回“未发布”,而应该把状态交给待复核分支。
这样拆开之后,日志和上游动作也会变得更清楚。上传成功但页面回读失败,是一种状态;页面回读成功且命中审核中,是另一种状态;页面回读成功但没有找到任何相关卡片,才需要进一步判断是延迟、筛选条件不对,还是确实没有发布。它们不能都叫失败。
发布系统要同时记录“做了什么”和“观察到了什么”,不能只保留最后一个结论。
这里的价值不只是方便排查。调用方真正需要的是下一步决策依据:可以安全重试,应该等待后复查,还是必须人工确认。没有分层状态,上游只能根据一个粗糙的真假值猜测,猜错就会把诊断问题变成业务副作用。
验证器要跟着页面的事实变化
这次修复的重点不是把旧选择器再写得更长,而是先做只读诊断。打开真实管理页,采集当前页面能看到的状态词、卡片结构和作品相关信息,再根据这些证据重写定位。页面上仍然存在“已发布”“审核中”“未通过”等状态词,这些词比旧版固定文案更接近业务事实。
我也会把卡片定位从单一结构改成几种可接受的路径。先找能代表作品的卡片,再在卡片内部查找状态词;如果页面结构发生轻微变化,只要业务信息仍在,就不应该立刻判定失败。定位失败时,可以在有界时间内重试,但每次重试都应重新读取当前页面,而不是对同一个失效选择器重复等待。
这里有一个容易被忽略的陷阱:上传中提示语可能长期驻留页面,不能把它当成唯一的完成信号。过去曾因为这个提示语一直存在,导致程序等待了900秒。等待时间变长,并不会自动增加判断准确性;如果观察对象本身选错了,等得越久只是越晚得到错误答案。
所以我会让验证器区分“过程提示”和“结果状态”。过程提示可以说明页面还在变化,但不能覆盖已经出现的已发布卡片;结果状态则应该优先根据当前作品卡和明确状态词判断。若两者同时出现,系统需要按业务优先级处理,而不是简单地看到“上传中”就继续等。
失败时要保护上游的手
一个成熟的验证器,不只负责告诉人发生了什么,还要约束上游接下来能做什么。对于发布类操作,我更愿意让结果至少有成功、确认失败、未知待复核3种,而不是只有成功和失败2种。成功意味着已经命中明确的已发布状态;确认失败意味着有足够证据说明没有完成;未知待复核则表示动作和业务状态之间还有一段没有读通。
待复核不是推卸责任。它需要带着证据返回:动作是否提交过,管理页是否打开,采集到哪些状态词,找到了几张相关卡,最后卡在哪个阶段。这样上游可以先做只读复查,而不是盲目重发。第9轮的真实管理页只读验证,恰好找到1张卡并命中已发布,这类证据才足以把状态从未知改成成功。
失败结果必须能指导安全动作,否则它只是把不确定性传给了下一个调用者。
我也会把“是否允许重试”单独建模。页面回读超时,不应该自动触发第2次发布;最多先重读管理页,或等待一段有界时间后再次查询。只有当只读证据明确表明没有产生作品,才考虑重新执行发布动作。重试策略因此不是“失败就再来一次”,而是“先确认上一次有没有留下结果”。
下次我会怎么做
下次遇到发布成功却报失败,我会先停住上游的重试动作,确认这次返回的失败究竟来自哪里。是发布动作没有接受,还是页面读取没有命中,还是业务状态本来就不是已发布?如果这3个问题没有分开,任何结论都不够可靠。
然后我会用只读方式打开真实管理页,采集当前页面的状态词和卡片结构,确认是否出现已发布、审核中或未通过。验证器会优先使用当前页面的业务事实,避免继续依赖已经变化的旧文案;等待也会设置边界,不能因为一个常驻的上传提示语把时间拖到900秒。
在代码层面,我会把发布动作、页面回读、业务状态判定保持为3层,并让每层留下自己的证据。验证器没有命中时,默认输出未知待复核,而不是诱导上游重发。修复完成后,还要同步执行副本和源码,再跑契约检查,确认真正生效的不是某一台机器上的临时改动。
这件事让我重新意识到,自动化的可靠不是“每次都返回一个答案”,而是答案的确定程度和动作风险相匹配。一个流程成熟的标志,不是它能在页面不变时跑通一次,而是页面变化、结果延迟或验证超时时,它仍然知道什么时候可以继续,什么时候必须停下来看看。