AI Agent 点了发布却超时:一条回执丢失,为什么能造成两篇文章?

AI Agent 点了发布却超时:一条回执丢失,为什么能造成两篇文章?

你让自动化助手发布一篇文章。页面上的按钮确实被点击了,随后控制接口却抛出超时异常。执行日志里只有一句"工具失败",文章后台却可能已经生成了真实记录。若调度器立刻重新进入编辑器,把同一份内容再提交一次,十几秒的网络异常就会变成两篇重复作品。更糟的是,系统可能同时把成功的业务标成失败,把健康的平台脚本列入维修清单,第二天继续重复犯错。

这是一个典型的分布式系统问题,而不只是浏览器自动化技巧。业务操作发生在远端平台,执行器收到的是另一条通信通道传回的消息;平台提交与回执到达不是同一个原子事件。本文用一个可在本地运行的 Python 实验说明:怎样记录稳定任务身份、区分执行结果与业务结果、只读核对远端副作用,以及在不启动第二套浏览器的情况下恢复任务。

先明确边界:文章中的站点、请求与故障注入均是演示场景,不代表某个具体平台实际发生过同样事故;示例程序也不替真实平台提供幂等保证。实际发布仍须遵循目标站点的用户协议和登录、安全验证要求。

一、超时只描述通信,不描述业务有没有发生

先设想一条完整的路径:调度器保存稿件,浏览器执行器点击发布,站点服务端接收请求,服务端写入文章记录,返回成功响应,浏览器控制服务将结果传给上层。只要其中一段返回消息晚于超时预算,上层就可能看到失败。然而文章创建动作可能早已完成,双方对同一次执行得到了不同的观察。

最容易混淆的情况共有三种。第一种是命令根本没有被送到远端,例如本地参数无法解析,连浏览器调用都没开始。第二种是点击已经发出,平台明确拒绝,例如必填栏目缺失,界面保持在编辑状态并给出确定的错误。第三种是点击发出之后上层断线,平台可能成功、可能失败,现有证据不足以决定。第三种必须称为"结果未知",不能偷换成"失败后可重试"。

上层观察 平台可能状态 允许的下一步
本地参数校验失败,确认命令未派发 平台尚未接收动作 仅修正无副作用的调用输入
平台明确返回校验拒绝且无记录产生 该次提交未接受 修复具体字段并重新预检
点击后控制器超时或断线 可能已生成文章 只读查询本人后台与目标文章
平台返回成功并可独立读回 已完成 记录回执,不再发布

这张表真正重要的不是异常名称,而是"有没有可靠的副作用证据"。一段 Python 栈追踪只能证明调用链的某处失败;它不会自动告诉你远端事务是提交前失败还是提交后丢包。使用浏览器截图、控制器状态、作者后台和公开链接的目的,就是把这几段事实拆开,而不是找到一个看起来像失败的字符串。

二、控制器、平台流程和业务终态必须分开

很多自动化系统用一层总异常处理包住整个发布脚本:只要任何函数抛错,就把当前平台任务标为失败。这样写省事,却会造成严重误归因。浏览器控制器连接失败、Windows 命令引号写错、页面短暂没有加载、平台内容审核不通过,是四种不同的问题,不应该落到同一个"发布 Skill 损坏"标签中。

建议把证据分成三层。控制层只管现有浏览器连接是否健康、真实标签是否还存在、当前页面是不是原来的页面;业务流程层只管标题、正文、图片、分类与一次提交动作是否符合当前平台约定;业务事实层只管文章是否真实存在、审核状态是什么、公开页面能否按原题找到。控制层健康不代表文章发布成功;脚本抛错也不代表文章一定没生成。

证据层 典型字段 不能据此推出
控制层 会话身份、受控页面、调用错误 平台文章已经失败
流程层 草稿编号、标题摘要、点击尝试标记 已经公开并通过审核
业务层 平台文章编号、准确标题、审核状态 执行器所有代码都正确

还需要区分"提交成功"与"公开通过"。有的平台在点击发布后先进入审核队列,平台明确接受稿件并生成稿件编号,这对每日提交任务可能已经满足合同;但它不是公开页面已对所有访客可见。回执里要把这两个字段单独记录。否则系统会为了等审核一直点击发布,或者反过来把审核中说成公开完成。

三、先给逻辑任务一个不会变化的身份

重复发布常常不是按钮本身的问题,而是系统根本不知道两次操作属于同一件事。错误做法是每次重试都生成一个新的随机任务编号;这样即使正文完全相同,任务账本也会把它们看成两个独立工作。合理的身份应绑定账户、目标平台、内容的稳定摘要与业务日期。摘要可以检测内容是否变化,但它不能替代平台的实际稿件 ID。

下面这段标准库 Python 示例会在当前目录创建独立演示用的任务回执文件。它采用排他创建,保证同一个标识在本地不会被无条件重新登记;文件内容写入后主动同步,以减少进程突然退出带来的缓冲丢失风险。注意:这只展示单台机器上的本地防重边界,不宣称多个主机或整个文件系统具备分布式事务能力。

python 复制代码
from pathlib import Path
import hashlib
import json
import os
from datetime import datetime, timezone

ROOT = Path("publish_demo_receipts")
ROOT.mkdir(parents=True, exist_ok=True)

def task_identity(account, platform, day, title):
    raw = "\n".join([account, platform, day, title])
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]

def arm_once(account, platform, day, title):
    key = task_identity(account, platform, day, title)
    path = ROOT / (key + ".json")
    receipt = {
        "task_id": key,
        "account": account,
        "platform": platform,
        "title": title,
        "state": "ATTEMPT_ARMED",
        "armed_at": datetime.now(timezone.utc).isoformat(),
        "remote_article_id": None,
    }
    try:
        with path.open("x", encoding="utf-8") as stream:
            json.dump(receipt, stream, ensure_ascii=False)
            stream.flush()
            os.fsync(stream.fileno())
    except FileExistsError:
        return {"allowed": False, "receipt": str(path)}
    return {"allowed": True, "receipt": str(path)}

if __name__ == "__main__":
    kwargs = dict(
        account="demo",
        platform="example",
        day="2026-10-11",
        title="一次提交实验",
    )
    print(arm_once(**kwargs))
    print(arm_once(**kwargs))

连续运行同一脚本,第一次得到允许,后续同一个任务标识会得到拒绝。它保守地防止了重复尝试,却无法告诉你远端发布是否成功,因此不能把本地拒绝当成业务成功。如果需要再次发布,必须先完成远端核实,并且根据平台明确证明原先没有产生副作用,不能为了让程序通过就把登记文件删除。

实际工程中还应该记录原始草稿 ID、目标账号 ID、标题哈希、最后观察到的页面地址和故障发生在什么阶段。为了避免泄露,日志不要包含登录 Cookie、访问令牌、验证码以及文章私人草稿的完整内容。系统需要能追踪同一业务,不等于需要永久保留所有页面数据。

四、远端核实要查"本人作品",不是搜索引擎缓存

当点击发布的结果不明时,最权威的证据通常是平台本人后台的已发表列表、草稿箱、审核列表和成功详情页。搜索引擎尚未收录并不能说明文章没有发布;公开页暂时不可见也可能是审核、缓存或访问验证导致。不同账户使用不同后台页面,不能把一份通用的公开搜索脚本当作全部平台的验收器。

核实时至少对照稳定身份、准确标题和时间范围。若后台出现与原稿一致的文章,继续读取其实际编号与状态,再根据业务合同判断完成程度;若只在草稿箱看到原稿,也不能立刻认定之前的点击从未触达服务端,还要确认审核队列、发布计数和页面当前提示。存在任何冲突证据时,宁可保留结果未知,也不要靠重复点击碰运气。

第二个示例用内存列表模拟作者后台回读,特意区分已发表、审核中、草稿和没有找到四种状态。代码完全离线、可直接运行,外部站点在真实项目中应通过其授权页面或 API 只读获取列表,再传给同样的判定逻辑。

python 复制代码
from dataclasses import dataclass

@dataclass(frozen=True)
class RemotePost:
    article_id: str
    title: str
    status: str

def reconcile(expected_title, records, listing_complete=False):
    same = [row for row in records
            if row.title.strip() == expected_title.strip()]
    if len(same) > 1:
        return {"state": "DUPLICATE_REQUIRES_REVIEW",
                "article_ids": [x.article_id for x in same]}
    if len(same) == 1:
        item = same[0]
        if item.status == "published":
            return {"state": "PUBLISHED_VERIFIED",
                    "article_id": item.article_id}
        if item.status == "reviewing":
            return {"state": "SUBMITTED_PENDING_REVIEW",
                    "article_id": item.article_id}
        if item.status == "draft":
            return {"state": "DRAFT_PRESENT",
                    "article_id": item.article_id}
    if not listing_complete:
        return {"state": "UNKNOWN_NEEDS_MORE_READBACK"}
    return {"state": "NOT_FOUND_CHECK_OTHER_QUEUES"}

if __name__ == "__main__":
    title = "一次提交实验"
    posts = [RemotePost("12345", title, "reviewing")]
    print(reconcile(title, posts, listing_complete=True))
    print(reconcile(title, [], listing_complete=False))

注意最后一个状态写的是"检查其他队列",而不是"允许重新提交"。即使 API 当前列表完整,仍应检查平台是否可能存在延迟提交、审核队列或异步作业;没有独立保证时,不能从列表空直接推断服务器从未接受请求。这个差异是防止重复发布的核心。

五、一次恢复只允许改变有证据的那一层

如果控制器的连接状态出现异常,先验证已有进程和真实浏览器标签,再尝试原连接允许的有限恢复,千万不要把每一次超时都解释为"需要新开浏览器"。多出一套控制器以后,两个标签可能同时持有同一份草稿,产生谁有权点击最终提交的问题。反过来,只要原会话还在,重新连接时就应该优先沿原会话、原草稿继续,不要为了操作顺利而把编辑器重新建一遍。

如果平台明确返回内容字段错误,修正那个字段。假设一个站点不接受包含连字符的标签,那么正确的范围是修改该站点当前文章的标签映射,然后重新核查目标文章是否已产生;这不是更改系统代理、重建控制器、重写所有平台脚本的理由。把错误分层能缩短维修时间,也避免一个局部变化污染几十个正常任务。

如果平台要求额外的邮箱验证,应识别并停在原页面,等待合法账户验证通过后继续原稿。该中间状态不等于文章不合格,也不能无条件跳过验证。账号验证、发布审核、控制连接、脚本业务门禁都有自己的责任边界。把四类状态合并为一个笼统的失败,很容易让调度器先放弃当前任务,最后留下很多永远无人核实的稿件。

发布完成后的复盘同样应该分层。一次成功的恢复如果来自控制器短暂恢复,就更新控制器故障说明;只有平台选择器真正失效才更新该平台步骤;如果问题是把未完成任务调度到下一项,就应修调度规则,而不是给每个平台额外叠一层判断。规则越多不一定越安全,只有实际改变下一次输入路径的规则才有价值。

六、把"能继续下一个"设计成业务门槛

一个发布任务可以设置准备中、一次尝试已登记、结果未知、平台已接受、公开已验证以及需要核对等状态,但真正允许调度器推进到下一编号的条件,必须与业务合同对应。平台要求公开发布就核对公开文章;平台支持投稿审核,允许以已接受且有编号作为当日提交成功;只保存草稿的平台就验证草稿箱,不得冒充已经发表。

每个阶段结束时保存最小回执。回执应包含任务编号、账号、平台、稿件身份、原始阶段结果、实际链接和独立验收结论。不要用"浏览器调用退出码为零"替换平台证据,也不要用"脚本报错"覆盖平台真实成功。失败回执可以保留为历史事件,但随后恢复成功的同一业务应以新的续接成功证据成为当天最终状态,这比偷偷改写旧日志更容易审计。

严格串行不意味着碰到网络故障就无限等待。可以设置合理的排障预算;超过预算后停留当前任务,保存草稿和下一步允许的只读检查,等待必要的账户操作或后续恢复窗口。但不能仅因为三十分钟过去,就把没有验收的任务记作成功并开始下一平台。进度数字越好看,遗漏的风险越大;真实完成一项比宣布处理十项更有意义。

七、发布前做三组故障注入,比事后喊"重试"有效

第一组测试是在命令发出前制造错误:例如输入参数解析失败、浏览器尚未连接、稿件字段缺失。预期结果是不产生任何平台副作用,也不会生成一个虚假的成功状态。第二组是在按钮点击后故意让回执通道中断,随后只恢复只读核验。预期是无论文章已经发表、进入审核还是仍在草稿,都不会自动触发第二次不可逆点击。

第三组是平台已经明确返回成功,但本地写回执失败。预期系统会补记录、修复本地状态,而不是重发文章来让账本变完整。还需要模拟两个会话同时领取同一个任务:只有持有正确唯一所有权的一方能编辑与提交,另一方只能观察或等待。这种测试比一句"避免重复"更能说明真正的防护范围。

上线前我会要求自动化流程回答几个具体问题:当前控制的是哪一个真实页面?稿件和账号是否与任务身份一致?是否已经存在不可逆尝试标记?平台后台是否看到了原文章?结果未知时有没有只读路线?执行器错误到底发生在哪一层?若这几个问题没有答案,就不应把下一次点击伪装成恢复。

能够稳定发布的 Agent,并不是从来没有遇到超时的 Agent,而是遇到超时后仍能保住业务身份,知道哪些事实已经确认、哪些尚未确认,并在证据不足时守住"只读、不重发"的边界。自动化真正节省的不是点按钮的那几秒,而是让每一次操作都可验证、可恢复、可追责。

参考资料:Python 标准库 os.replace 文档 docs.python.org/3/library/o... 文件操作文档 docs.python.org/3/library/p... 方法幂等语义见 RFC 9110 www.rfc-editor.org/rfc/rfc9110...

相关推荐
怪奇云呼军1 小时前
教育试听预约后没到课,闪电智能 Voice Agent 如何做分层回访?
人工智能·python·算法·云计算·音视频
ajassi20001 小时前
AI语音智能体开发日记(十八)智能体服务器xiaozhi-esp32-server源码部署指南
运维·服务器·人工智能·ai·ai编程
云淡风轻~窗明几净2 小时前
角谷猜想的进展之三 移动了群山/沸騰的群山 2026-10-10
人工智能·算法·dubbo·图论
AliCloudROS2 小时前
计算巢支持一键私有化部署Qwen3.8-27B
人工智能
汇智信科2 小时前
汇智兵棋:AI赋能的一体化智能兵棋推演平台
人工智能
Joecien2 小时前
AI 生成透明背景图实战:Qwen-Image-2.1-Pro API 实测(21 次实验 + 完整调用代码 + 避坑指南)
人工智能
weixin199701080162 小时前
《淘宝TOP API:落地方案、接口边界与业务踩坑 —— 聚石塔内外价差 10 倍的真相》(附 Python 源码)
开发语言·python
龙亘川2 小时前
设备管理业务建模:从设备信息台账到维护计划执行闭环
大数据·人工智能·智慧城市·开源软件·数据可视化
梦想的颜色2 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus