别再迷信一键发布:打造不重发、不泄密、可回查的 AI 内容投递管道
很多开发者第一次做内容自动化时,脑子里的画面都很简单:AI 生成一篇草稿,调用一个 API,内容就出现在博客、社区或社交平台上。
但真实项目很快就会遇到一堆麻烦:
- 有的平台要用 OAuth,有的平台只认 API Key,有的平台还得靠浏览器会话。
- 有的接口创建的是草稿,有的接口会直接公开。
- 接口返回成功,不等于读者已经看到内容,可能只是“已接收,正在处理”。
- 请求超时后要不要重试?如果上一次其实已经发出去了,重试会不会造成重复内容?
- 密钥放在哪里?日志里会不会泄露?
所以,我更建议把这件事看成一个小型、可观察、可恢复的投递管道,而不是一个神秘的“一键发布”按钮。
别把所有平台硬塞进同一个 publish()
最省事的写法,是把所有平台都藏在一个通用 publish() 后面。接口看起来很漂亮,但这是在假设所有平台行为一致。现实恰恰相反:有的平台支持草稿,有的平台支持定时,有的平台支持封面图,有的平台根本不支持某些字段。
更稳的做法是:保留一套共享的投递契约,同时允许每个平台在自己的适配器里处理差异。共享层描述内容本身,适配器决定目标平台到底能做什么。
// 发布请求:一次投递需要的最小信息
type DeliveryRequest = {
// 内容题目
title: string
// 正文 Markdown
markdown: string
// 标签
tags: string[]
// 可见性:草稿或公开
visibility: "draft" | "public"
// 原文地址,可选
canonicalUrl?: string
// 幂等键:防止重试造成重复发布
idempotencyKey: string
}
// 发布结果:不是只有成功或失败
type DeliveryResult = {
// 远端平台返回的唯一 ID
remoteId?: string
// 最终可访问地址
url?: string
// 状态:排队、草稿、已公开、失败、未知
state: "queued" | "draft" | "published" | "failed" | "unknown"
// 原始状态,便于排查
rawStatus?: string
// 错误信息
error?: string
}
// 连接器:每个平台实现自己的投递逻辑
interface PublisherConnector {
// 发布前检查
validate(input: DeliveryRequest): Promise<string[]>
// 执行投递
deliver(input: DeliveryRequest): Promise<DeliveryResult>
// 查询远端状态
inspect(remoteId: string): Promise<DeliveryResult>
}
这里最值得注意的,不是字段有多少,而是 unknown 状态。很多发布事故都发生在请求超时之后:客户端不知道远端有没有收到,如果立刻重试,就可能重复创建。把状态标记为 unknown,然后安排一次回查,比盲目重试安全得多。
凭证不要进入内容流程
很多发布系统看起来功能没问题,最后却倒在安全审查上。原因往往不复杂:密钥被粘贴到提示词里、写进日志、放进截图、和生成内容存在一起。
内容处理 Worker 不应该直接拿到密钥。它只需要知道“用哪个连接器”,真正读取密钥的位置应该放在适配器内部。写日志前,也要把 Authorization 相关字段脱敏。
还有几个很实用的原则:
- OAuth refresh token、API Key、应用密码、浏览器会话,不要都叫“token”。它们的轮换方式、过期方式、风险等级都不一样。
- 只申请最小权限。一个只会创建内容的流程,不应该自动拿到账号管理权限。
- 如果平台把草稿创建和公开发布分开,权限也尽量分开。
- 如果必须依赖浏览器会话,要把它当成会过期的临时凭证,而不是永久集成。定时任务开始前做一次健康检查,远比跑到一半发现会话失效更稳。
内容先冻结,再进入发布队列
一篇内容进入发布队列前,最好先变成一个不可变修订。否则会出现很尴尬的情况:任务正在重试,编辑又改了题目,两个平台收到同一个任务的不同版本。
一个实用的记录可以包含:
- 源版本
- 渲染后的 Markdown
- 媒体引用
- 目标平台策略
- 内容哈希
投递任务只指向这个快照。后续再修改,就生成新的修订,而不是悄悄修改一个正在执行中的请求。
这样做还有一个好处:人工审核和无人值守自动化可以共用同一套管道。团队可以要求公开内容必须先审核,但审核通过后,依然走同样的投递机制,不需要维护两套系统。
发布前的策略检查也应该放在这一层。比如:
- 题目长度是否超限
- 标签数量是否超限
- Markdown 中是否有目标平台不支持的内容
- 图片是否可访问
- 目标平台是否允许当前可见性
这些问题应该在调用远端接口前就被发现,并以编辑反馈的形式返回,而不是等到 API 报错才补救。
重试要无聊,甚至无聊到没有故事可讲
演示系统和生产系统的分水岭,就在重试。
先生成一个稳定的幂等键。它可以由目标平台、账号、内容修订版本共同生成,并在发起远端请求前持久化。如果平台支持原生幂等请求头,就用平台能力;如果不支持,就维护本地投递账本。重试前先查已有远端 ID,不要直接再发一次。
不是所有错误都值得重试:
- 网络抖动、限流、服务端 5xx,可以做带随机抖动的指数退避。
- 凭证失效,应该暂停连接器,并发出可处理告警。
- 内容被平台拒绝,需要编辑修改,不是反复重试。
一个足够简单的 Worker 循环可以这样写:
// 调用连接器,把内容投递到目标平台
const result = await connector.deliver(job.payload)
// 根据返回状态决定下一步,不要把所有响应都当成成功
switch (result.state) {
// 已公开或已存为草稿,可以结束任务
case "published":
case "draft":
await complete(job.id, result)
break
// 平台还在处理,或者结果未知,需要稍后回查
case "queued":
case "unknown":
await scheduleInspection(job.id, result.remoteId)
break
// 明确失败时,再按错误类型决定是否重试
case "failed":
await applyRetryPolicy(job.id, result.error)
break
}
这里的关键是:提交和验证是两件事。远端平台可能接收了内容,但还在异步处理;也可能先审核,再公开。最终 URL 和真实可见性,比单纯的 200 OK 更可信。
观察结果,不只看请求是否发出
有用的发布日志,要能回答普通运维问题:
- 哪个版本被发出去了?
- 发到了哪个账号、哪个平台?
- 是草稿还是公开?
- 返回了什么 URL?
- 回查花了多久?
- 为什么触发重试?
- 重试是否安全?
建议保留结构化事件链,而不是只记一句 success。例如:
validatedsubmittedremote_id_receivedvisibility_checkedpublished
这样排查问题时,链路会清楚很多。
发布后最好再做一次回读:重新拉取远端内容,核对题目、原文地址和可见性。有些平台会转换 Markdown、过滤 HTML、拒绝媒体,接口看起来成功了,页面却不是你想要的样子。
连接器不是产品,可靠管道才是
当管道稳定之后,更换一个平台适配器会变成常规工作。真正有价值的是外围系统:不可变修订、最小权限、明确状态流转、安全重试、平台感知校验、可验证结果。
这套设计也能把 AI 放在正确的位置。模型可以负责调研、写作、改写、调整语气,但不应该负责猜测一个超时请求到底有没有造成公开内容。确定性的代码、持久化状态和回查机制,更适合处理这些脏活。
好的内容自动化,不是发得更快,而是发得足够稳定:没有重复内容,没有泄露密钥,没有神秘的“成功任务”,也没有每次运行后都要人工巡检各个平台的疲惫。最终目标不是让团队天天盯着发布系统,而是让发布稳定到可以被忽略。