n8n实现Misskey向Mastodon、GoToSocial及Memos的多端同步

前言

25年下半年,我才第一次接触联邦宇宙的概念。

以下摘自 联邦宇宙 - 维基百科,自由的百科全书

联邦宇宙(英语:Fediverse,简称Fedi)在英文中是“联邦”(Federation)和“宇宙”(Universe)的混成词。
联邦宇宙由一系列自由软件组成,有一组互联的服务器(用户自建或第三方托管),一起提供网络发布(如社交媒体、微博、博客或者网站)或者文件托管功能。
虽然各个服务器是独立运行的,且各个实例繁多,内容多样, 但服务器之间可以彼此互通。
在不同的服务器(实例)上,用户可以创建不同帐号,因为服务器上运行的软件支持一种或多种遵循开放标准的通信协议,能够跨越实例边界而通信。
与在单一服务器上运行的传统社交网络相比,联邦宇宙的运行方式更开放,其服务器的分散性,使联邦宇宙更安全可靠。

从一开始接触,我就对此概念非常感兴趣,并尝试搭建自己的联邦宇宙服务器,12月底,从较为复杂的Mastodon(又称乳齿象、长毛象或万象)开始,到Misskey,再到最简单占用资源最小的GoToSocial,一个个都搭建体验了一遍,各有优缺点,还有于25年6月初就已搭建的memos 0.18.2服务,虽无联邦的功能,但同样属微博、说说类型,一时间竟无法确定最终使用哪个系统。

刚好,同一时期接触到了n8n工具,就决定以系统占用量不如Mastodon大、功能比GoToSocial更丰富的Misskey作为发布端,利用自动化工作流工具n8n,实现多端同步功能。

在这篇文章中,将分享如何通过 1Panel 部署 n8n,并记录一下同步工作流针对不同平台特性遇到的一些典型问题。

一、 n8n 的部署与关键配置

我的 n8n 是通过 1Panel 应用商店安装的。虽然应用商店的一键部署很方便,但为了让 n8n 的 Webhook 能够被外部正确识别,以及优化系统资源,建议安装前自定义 docker-compose.yml 文件。

1. 为什么需要自定义配置?

在默认安装下,n8n 的 Webhook 节点生成的 URL 往往是内网 IP 或 localhost,会导致 Misskey 无法回调成功。因此,我们需要通过环境变量 WEBHOOK_URL 明确告诉 n8n 的对外访问域名。

同时,为了减少控制台不必要的报错日志并保护隐私,关闭了诊断数据上传。

2. docker-compose 配置

目前正在使用的配置片段,关注环境变量:

networks:
    1panel-network:
        external: true
services:
    n8n:
        container_name: ${CONTAINER_NAME}
        deploy:
            resources:
                limits:
                    cpus: ${CPUS}
                    memory: ${MEMORY_LIMIT}
        environment:
            # 必须设置为 false,配合反代使用
            N8N_SECURE_COOKIE: false
            # 指定外部访问的域名,否则 Webhook 无法被正确触发
            WEBHOOK_URL: https://n8n.xiaoten.com/
            # 关闭诊断数据,减少日志干扰
            N8N_DIAGNOSTICS_ENABLED: false
        image: n8nio/n8n:2.2.2
        labels:
            createdBy: Apps
        networks:
            - 1panel-network
        ports:
            - ${HOST_IP}:${PANEL_APP_PORT_HTTP}:5678
        restart: always
        volumes:
            - ./data:/home/node/.n8n

配置完成后,重启容器,会发现 Webhook 节点里生成的 URL 变成了自定义域名 https://n8n.xiaoten.com/...

二、 工作流逻辑备忘

整个工作流的核心逻辑并不复杂:Webhook 接收 -> 过滤 -> 下载图片 -> 分发到各平台。

整个流程图如下:

1. 过滤逻辑:识别用户ID

Misskey 的 Webhook 会推送所有动态。我们需要 If 节点 进行过滤。 在 n8n 的 JSON 结构中,我是这样配置 conditions 的:

{
  "parameters": {
    "conditions": {
      "combinator": "and",
      "conditions": [
        {
          // 确保 userId 是我自己
          "leftValue": "={{ $json.body.body.note.user.id }}",
          "rightValue": "这里填你自己的Misskey_User_ID", 
          "operator": {
            "type": "string",
            "operation": "equals"
          }
        },
        {
          // 确保不是回复 (Reply)
          "leftValue": "={{ $json.body.body.note.replyId }}",
          "rightValue": "",
          "operator": {
            "type": "string",
            "operation": "empty",
            "singleValue": true
          }
        },
        {
          // 确保不是转帖 (Renote)
          "leftValue": "={{ $json.body.body.note.renoteId }}",
          "rightValue": "",
          "operator": {
            "type": "string",
            "operation": "empty",
            "singleValue": true
          }
        }
      ]
    }
  },
  "type": "n8n-nodes-base.if",
  "name": "是否是xiaoten发新帖"
}

可视化界面上如下设置:

这样保证只有指定用户ID发布的帖子才会被同步出去。

2. 同步到 Mastodon

Mastodon 的 API 比较标准。如果是纯文本,直接发帖即可;如果是带图内容,流程如下:

  1. 下载图片:n8n 先将 Misskey 的图片下载到内存。

  2. 上传媒体:调用 Mastodon 的 /api/v2/media 接口上传图片,获得 media_id

  3. 合并 ID:如果是多图,需要将多个 ID 合并为一个数组。

  4. 发布状态:调用 /api/v1/statuses,将正文和 media_ids 一起发送。

这部分比较顺畅,难点在于合并 ID。如果发送多张图,n8n 会运行多次上传节点,这时则需要一个 Code 节点 来把这些零散的运行结果聚合起来:

// Merge Mastodon IDs 节点代码
const items = $input.all();
// 按照原始顺序提取 ID
const sortedIds = items
  .map(item => ({
    id: item.json.id,
    index: item.pairedItem.item 
  }))
  .sort((a, b) => a.index - b.index)
  .map(data => data.id);

return { json: { media_ids: sortedIds } };

3. 同步到 GoToSocial

一开始我将 GoToSocial (GTS) 实例搭建在一个非24小时运行的服务器上,如果不做处理,一旦 GTS 掉线,整个 n8n 流程就会报错停止。

虽然现在已经将此实例转移到一个持续在线的服务器上,但为了防止之间干扰,实现方式还是保留了。

我是参考了“探针”逻辑,检测GTS实例的在线状态,来进入对应的工作流,防止整个工作流报错停止。

  1. Check GTS Status: 在正式发送前,先用一个 HTTP Request 节点去 ping 一下 GTS 的实例信息接口 /api/v1/instance关键配置:必须开启“错误时继续执行”。

    // 节点配置片段
    {
      "url": "https://social.poto.top/api/v1/instance",
      "options": {
        "timeout": 3000 // 3秒超时,防止卡死
      },
      "onError": "continueRegularOutput" // 报错不要停!
    }
  2. Is GTS Online? (If 节点): 紧接着判断上一步的结果。

    • 条件:判断返回的 uri 字段是否不为空

    • True:说明 GTS 在线,执行后续的发图/发帖流程。

    • False:说明 GTS 挂了,直接走 NoOp (空操作) 分支,跳过 GTS 同步。

通过这个简单的逻辑判断,即便 GTS 不在线,也不影响其他同步流程。

4. 同步到 Memos

Memos (v0.18.2) 的 API 在处理多张图片时,如果并发上传,返回的 Resource ID 顺序往往是乱的。会导致在 Misskey 发了“图1、图2、图3”,同步到 Memos 变成了“图2、图1、图3”。

为了保证顺序,引入了 Loop Over Items (SplitInBatches) 循环节点:

  1. 切片循环:将图片数组拆分,强制 n8n 每次只处理一张图片

  2. 顺序上传:在循环中调用 Memos 的上传接口。

  3. 重组 Markdown:

    这是最关键的一步。循环结束后,我使用一个 Code 节点,将所有上传成功的 ID 重新提取出来,并按照原始顺序拼接成 Memos 支持的 Markdown 格式:

    // Code 节点逻辑片段
    const mediaMarkdown = items.map(item => {
      const filename = item.json.filename || 'image';
      const id = item.json.id;
      // 生成 Memos 标准图片语法 ![name](resources/id)
      return `![${filename}](resources/${id})`;
    }).join("\n");

通过这种排队上传的方式,解决了图片乱序的问题。

三、 总结

通过 n8n 将 Misskey 作为内容源分发,其主要逻辑非常清晰,但是针对不同平台的部分特征有不同的处理方法,但其处理方法均能够借助AI轻松实现,本次也是试验了一下该想法的可行性,正常情况也不会同时维护这么多平台。

因为以上需求,算是对n8n有了非常简单的入门,因为用到的是最基本的功能,甚至还没有触及其核心功能(AI Agent)。n8n可实现的功能远不止于此,还有待进一步学习。

评论