多平台内容分发的适配器

当一个平台不给你 OAuth 时:多平台内容分发的适配器设计

做多平台内容分发,最麻烦的从来不是"发"这个动作,而是十几个平台各自为政:授权方式不同、内容格式不同、发布结果什么时候算"成功"也不同。

我们团队在做这件事时,把这些差异收敛成了一套「适配器 + 注册表」结构。这篇文章拆解它的设计,重点讲三个很少有人提、但真正决定这套系统能不能跑的点:

  1. 平台不给你 OAuth 怎么办
  2. 发布结果怎么算"成功"
  3. 异步流水线怎么保证不重复发

代码是开源的(MIT),下文所有结论都能通过读源码验证。


一、接口层:五类能力契约

先说最核心的抽象。platforms.interface.ts 里定义了五类能力:

能力 provider 职责
授权 auth OAuth 授权与刷新
发布 publish 内容发布(图文 / 视频各自适配)
回查 work 发布后状态确认与任务补偿
互动 engagement 评论、点赞等
数据 analytics 数据回收

每个平台一个目录,注册表统一映射:

bash 复制代码
src/core/channels/platforms/
├── bilibili / douyin / facebook / google-business
├── instagram / kwai / linkedin / pinterest
├── rednote / threads / tiktok / twitter
├── wechat / youtube
└── platforms.registry.ts      # 平台 → provider 的映射
    platforms.interface.ts     # 接口契约

看起来很规整,但没有哪个平台真的五件套全实现。我统计了仓库的实际覆盖:

平台 已实现的 provider
twitter auth · publish · work · engagement · analytics(最完整)
youtube auth · publish · work · engagement · analytics · webhook
bilibili auth · publish · work · analytics · webhook
kwai auth · publish · work · analytics
douyin auth · publish · work · webhook
rednote publish · work

小红书目录下只有两个 provider ------ 不是偷懒,是它压根没开放标准的 OAuth 授权。这就引出了第一个真正的问题。


二、平台不给你 OAuth 怎么办

这是做国内平台适配最硬的一道墙。很多平台没有面向创作者的公开 API,授权这件事只能各显神通。

项目里的 AuthType 枚举把这件事写得很直白:

ts 复制代码
export enum AuthType {
  OAuth2 = 'oauth2',
  Browser = 'browser',
  Plugin = 'plugin',
  QrCode = 'qrcode',
}

四种授权方式,正好对应四种现实:

  • OAuth2:标准授权码流程,能走这条路的平台最省心(twitter、youtube 等)
  • Browser:平台没有开放 API,只能复用浏览器里的登录会话
  • Plugin:通过浏览器插件在用户已登录的页面上下文里完成操作
  • QrCode:扫码登录,常见于微信系平台

小红书的 rednote 目录里连 auth provider 都没有,就是因为走不了 OAuth2 ------ 授权被上移到统一的授权服务里完成,适配器只负责拿到凭据之后的事。

这里有个工程上的取舍值得说:把"拿凭据"和"用凭据"拆开,好处是适配器变薄、平台差异收敛;代价是引入了一个集中式的授权依赖,账号凭证的托管责任也随之集中。这不是纯技术问题,选之前要先想清楚能不能接受这个责任。


三、发布结果怎么算"成功"

第二个容易被低估的问题:调用发布接口返回 200,不代表内容真的发出去了。

视频平台尤其典型------接口返回后还有转码、审核,几分钟后才真正可见。如果一律按"返回即成功"处理,用户会看到一堆假的成功状态。

项目里的 CompletionStrategy 枚举覆盖了五种情况:

ts 复制代码
export enum CompletionStrategy {
  Sync = 'sync',                    // 同步返回即完成
  Polling = 'polling',              // 需要轮询查询状态
  MediaFinalize = 'media_finalize', // 媒体需二次确认
  Webhook = 'webhook',              // 平台回调通知
  UserHandoff = 'user_handoff',     // 交回用户手动完成
}

这就是为什么不同平台的 provider 组合长得不一样 ------ 需要 webhook 的平台才有 webhook provider,需要轮询的才有对应逻辑。不是设计不统一,是现实不统一。

这个枚举本身是个不错的设计模式:与其在代码里写 if (platform === 'xxx') 判断怎么确认结果,不如把"完成方式"声明成平台的一项配置。新增平台时,先回答"你怎么告诉我发成功了",再决定实现哪些 provider。


四、异步流水线:三段 consumer 与幂等

发布不能同步阻塞。项目把发布拆成三段异步任务,走 BullMQ 队列:

sql 复制代码
publish  →  media-finalize  →  update-published
  ①            ②                   ③
提交发布      媒体处理与终态确认    回写发布结果与链接

对应 publish/consumers/ 下的三个文件:publish.consumer.ts、media-finalize.consumer.ts、update-published.consumer.ts。

几个关键设计:

  • 任务持久化在 Redis,worker 可以水平扩展,平台 API 限流时排队而不是丢弃
  • 用 redlock 做幂等,保证同一任务不会被多个 worker 重复消费------发布这件事,重复发一次比失败更难收拾
  • 媒体文件走对象存储(自托管 RustFS,也支持 S3 / OSS),通过签名 URL 访问,大文件不经过 Node 主进程内存

这样设计的实际意义:某个平台 API 抽风时,任务会重试而不是丢失,用户看到的是"发布中"而不是"失败"。


五、服务拆分与 MCP 封装

后端是 Nx 管理的 monorepo,两个应用 + 15 个共享库:

bash 复制代码
project/aitoearn-backend/
├── apps/
│   ├── aitoearn-server    # 业务主服务:渠道、发布、内容、用户
│   └── aitoearn-ai        # AI Agent 编排服务
└── libs/                  # 15 个共享库

为什么要拆两个服务 :aitoearn-server 管业务,aitoearn-ai 管模型调用。拆开的直接收益是 AI 模型故障不拖垮发布主链路------生成模型超时、限流、挂掉,都不影响已经排好队的发布任务。aitoearn-ai 里还有 ai-availability 模块做模型可用性探测,某个模型不可用时可切换。

共享库里有个值得单独提的:nest-mcp ,把 MCP 服务端封装成 NestJS 模块。目录里有 decorators / transport / adapters / mcp.module.ts,加一个 MCP tool 基本就是加个装饰器的事。

如果你的服务本身是 NestJS 写的,又想给 AI 助手暴露工具,这个封装思路可以直接借鉴------比从零接 MCP SDK 省不少事。

本地跑起来

装好 Docker 后三条命令,不需要手动装数据库:

bash 复制代码
git clone https://github.com/yikart/AiToEarn.git
cd AiToEarn
docker compose up -d

compose 会把前端、后端、MongoDB 副本集、Redis、对象存储一起拉起,副本集初始化也包含在内,起来后访问 localhost:8080。

⚠️ 生产部署前请改掉默认配置:docker-compose.yml 里数据库用的是示例密码,并且默认把 27017、6379 等端口映射到了宿主机。


六、这套设计的代价

写了这么多优点,也得说清楚代价是什么:

  1. 14 个适配器的维护是长期负担。平台 API 变化快,一个平台改版就得跟着改,这套结构只是让改动"不污染核心",没有让改动"消失"。
  2. 能力覆盖天然不齐。从第一节那张表能看出来,海外平台能力完整,国内平台普遍只有发布和回查。这不是代码问题,是平台开放程度决定的。
  3. 授权集中化带来责任集中。第二节说过,这里不再重复。
  4. 测试还没进 CI 门禁 。仓库里有不少 spec 文件,但 CI 目前只跑 lint 和构建,不执行测试(backen-check.yml 里只有这两步;前端 web-check.yml 的 ESLint 步骤还是注释掉的)。这意味着改动无法被自动验证------对我们自己、对外部贡献者,都是实实在在的阻碍。

第 4 条是我们目前优先级最高的技术债。如果你打算 fork 二次开发,建议先把测试门禁自建起来再改代码,否则回归风险不可控。


最后

回过头看,这套设计里真正有用的不是"五类能力"这个分类本身,而是两个把不确定性显式化的动作:

  • 把"平台怎么授权"声明成 AuthType,而不是散落在业务代码里的判断
  • 把"怎么算发布成功"声明成 CompletionStrategy,而不是默认"返回 200 就是成功"

多平台适配这件事,本质是在跟不确定性打交道。能收进配置表的就别写进 if,能声明的就别假设。

你如果在做类似的事,尤其是国内平台适配,欢迎在评论区聊聊踩过的坑。

相关推荐
u1301301 小时前
GitHub 热榜项目:日榜(2026-11-02)
人工智能·github
Sakura_j_h_2 小时前
GitHub Actions 打包 Windows exe:中文编码与产物上传
windows·ci/cd·打包发布·github·pyinstaller·github actions
miofly2 小时前
Meta 816B 参数 Muse Spark Open 在 NVIDIA GB300 上实现每 GPU 50K tokens 每秒
开源·github
dong_junshuai3 小时前
每天一个开源项目#148 T3 Code:把代理会话与代码工作区分开管理
github
徐小夕4 小时前
JitKnow知识库4.8发布:本体+知识图谱检索引擎,让AI回答不再“一本正经地胡说八道”
前端·vue.js·github
GitHub实测笔记4 小时前
ripgrep 本机实测:3.6G 目录 grep 7.4 秒、rg 0.12 秒,但 62 倍里有水分
github
believed_breadfruit6 小时前
我开源了一个HTML 分页导出工具,解决了复杂页面的内容截断、跨页错乱和长文档白屏问题
前端·javascript·github
不是万能的豆学长7 小时前
开源项目 public-apis 登顶 GitHub 趋势榜
开源·github
小弥儿7 小时前
38K星的开源项目 t3code:给 Claude Code 和 Codex 装一个手机遥控器
学习·智能手机·开源·github