界面说「已保存」,接口说「每天只能发 5 篇」:一次发布失败的完整取证

界面说「已保存」,接口说「每天只能发 5 篇」:一次发布失败的完整取证

下午三点,我往简书补发第五篇文章。脚本正常退出,日志里该有的都有:草稿建好了、标题设上了、正文粘进去了、保存接口返回 200、发布按钮的点击也确实触发了。编辑器右下角安安稳稳显示着「已保存」。

然后文章没上线。

这篇文章记录接下来四十分钟里我是怎么把原因找出来的。结论不重要,过程里那几个「我差点就信了」的瞬间比较有价值。

一、先把证据原样贴出来

不加工、不截取,脚本输出全文:

sql 复制代码
CREATE {"status":200,"id":144961809,"slug":"bca3fa64eebe"}
TITLE SET=别信文案,信接口:给自动化脚本装上「接口级断言」
AREA {"x":1024,"y":224}
PASTE_LEN 2322
SAVE PUT=200 {"id":144961809,"content_updated_at":1792127513,"content_size_status":"fine"}
SERVER {"status":200,"title":"别信文案,信接口:给自动化脚本装上「接口级断言」","contentLen":3187}
PUBLISH PUBLISH_CALLED action=publicize

七行,每一行单看都像成功。唯一的不协调是:作者页文章数还是 13,没有变成 14。

注意这里已经出现了一个反例。「保存成功」和「发布成功」是两件事,而我手里只有前者的证据。保存接口返回 200,只能证明草稿存住了,证明不了任何关于发布的事。

二、逐个排除,别跳步

排除一:点击根本没生效?

第一反应是脚本点了但没点动。简书的发布按钮在顶部导航里,之前踩过hover 浮层遮挡的坑。

重试三次,每次都打印 CALLED------React 的 fiber 链里确实找到了 onClick 并调用了,容器节点也拿到了。三次都一样。

不是点击问题。 但如果我这时候停下来说「点击生效了所以应该成功了」,就错了。点击生效只说明前端函数被调用,不说明服务端接受了请求。

排除二:内容超了链接数?

简书对非会员有链接数限制,这是已知约束。我之前按 markdown 源文清洗过,但简书实际是在渲染后的 HTML 加 innerText 上计数的------这两者不是一回事。

于是直接在编辑器里量。把官方同款计数正则跑在渲染结果上:

ruby 复制代码
COUNT {"htmlLen":3187,"textLen":2322,"filteredCount":0,"urlMatches":[]}

零。不是链接问题。

这一步顺带纠正了一个方法论错误:我原来只检查源文件,源文件干净不等于渲染后干净。虽然这次两边都是 0,但检查的位置本来就选错了。

排除三:索引延迟?

掘金那边真实遇到过:发布成功、接口回执 err_no:0,一分钟后查文章页是「找不到页面」,再查标题就正常了。所以发布后立刻查不到,不能直接判定失败。

等。等了五分钟,作者页还是 13 篇。

不是延迟。 延迟会自愈,这个没有。

排除四:我在用弱判据骗自己

我原来的判据之一是抓页面上的提示文案。日志里确实抓到了「发布成功」三个字------我当时挺高兴。

然后才发现:那是我稿子正文里引用的词。这篇稿子讲的就是发布成功与失败,正文里到处都是。我抓 toast 的正则把正文区也扫进去了。

文案判据的问题从来不是「经常错」,是它错了你不知道。这次它错得很漂亮:抓到了一个语义上完全合理的成功信号,而事实正好相反。

四、去听接口怎么说

前端该看的都看完了,只剩一条路:把发布这个动作发出的请求和响应抓下来。

挂 fetch 与 XHR 的钩子,记录 URL、状态码和响应体,然后再点一次发布:

swift 复制代码
CLICK CALLED
NETLOG [{"u":"/author/notes/144961809/publicize","s":403,
         "b":"{\"error\":[{\"code\":2016,\"message\":\"公开文章发布失败,现在每天只能发布 5 篇公开文章哦,请明天再来吧。\"}]}"}]
DIALOG ["字数:1459","已保存"]

原因一句话就说清楚了:简书非会员每天只能发布 5 篇公开文章,今天已经发满了。

而同一时刻,界面上显示的是「已保存」。没有任何 toast、没有任何红字、没有任何模态框。服务端把原因写得明明白白,前端一个字都没往外露。

五、这件事的通用形状

把这次的取证过程抽象一下,是个可以复用的结构:

判据 这次的表现 结论
退出码 / 日志无 ERROR 全绿 弱,只证明脚本自己没崩
接口返回 200 保存 200 中,但只覆盖被调用的那一个接口
DOM 状态(按钮可点、fiber 有 onClick) 三次 CALLED 中,只证明前端函数被调用
页面文案 抓到「发布成功」(其实是正文) 最弱,且会主动骗人
服务端响应体 403 + error 2016 + 完整中文原因 强,真相在这里

排班的顺序也有讲究:从便宜到贵,从本地到远端。先看日志(不要钱),再看 DOM(不要钱),最后才上网络抓包(要写代码)。但不管前面几层给出了多好的信号,只要终态不对,就得继续往下走。

这次我卡了很久,是因为第三层和第四层都给出了「合理」的信号,而它们合起来仍然不构成发布成功的证据。合理不等于发生。

六、两条能直接抄走的规则

规则一:发布类动作,判据必须落在「发布」那个接口的响应体上,不能落在保存、草稿、编辑任何邻近接口上。

这次我有四个 200:建稿 200、设标题成功、保存 200、读取 200。全是真话,但全都不是答案。它们描述的是这条链路上的其他环节。

规则二:服务端返回错误时,响应体比状态码更重要,比界面重要得多。

403 只是个状态码。error code 2016 加上那句中文,才是能直接指导下一步的信息------它告诉我这不是故障,是配额,明天再来就行,不用改任何代码。

如果我当初看到「点击生效但文章没上线」就开始改脚本,现在大概已经在重写发布逻辑了。而真正该做的只是等一天。

七、尾声

原因找到之后,事情变得很简单:把剩下的稿子排进每日队列,让自动化在额度刷新后接着发。

但我更想记下的是那个瞬间------日志里躺着「发布成功」四个字,我挺高兴,然后发现那是自己稿子里的词。

你最信任的那个信号,可能正是你自己写进去的。

相关推荐
用户539418729072 小时前
别只在 prompt 里让 AI 别碰文件:我给仓库装了四道提交闸门,越界直接拒绝提交
ai编程·cursor
ZzT2 小时前
Claude Code 团队的代码评审:合并量翻倍,回滚率降了 40%
ai编程·claude
HjhIron2 小时前
从朴素RAG到Agentic RAG(二):基于LangGraph的混合检索、重排与闭环设计
ai编程
坊钰2 小时前
【LangGraph框架入门级】12. 人机交互:interrupt 中断与 Command 恢复
开发语言·数据库·python·人机交互·ai编程·langgraph
Solara2 小时前
一个标志位挂了 18 天不落地:它等的重启,2 天 14 小时后就完成了
人工智能·程序员·ai编程
SkyWalking中文站2 小时前
从模型输入到文件变更:追踪 AgentCore 上的 LangGraph Agent
langchain·aigc·ai编程
金銀銅鐵3 小时前
[Java] 用 GUI 展示 ArrayList 扩容的过程
java·后端·ai编程
NC 王炸3 小时前
AI 做的便签,换个浏览器就没了?加一个导出按钮,把数据带走
javascript·ai编程·数据备份·localstorage·txt导出