使用 n8n 实现 Discord 消息自动化同步至 WebDAV

2025-08-17

背景

我之前通过PipeDream + Make.com构建了discord -> obsidian工作流,但PipeDream每月额度有限,在测试阶段就几乎耗尽,而Make.com虽无明显额度限制,却对工作场景有严格要求,且Watch Channel Message一个工作流仅能监控一个频道,所以我不得不另寻出路。经过一番周折,我又回到了 n8n

一、从 Discord 到 n8n

n8n 缺少原生的 Discord TriggerAction只能选择On Demand触发,无法实现发送新消息自动转发

解决方案:通过一个外部 Python Bot 监听 Discord 事件,主动推送数据至 n8n 的 Webhook 节点

1.1 n8n Webhook 配置

首个节点为 Webhook,生成一个唯一的 URL 作为数据入口,等待请求

1.2. Discord Bot 与 Python 脚本

  1. Bot 创建: 在 Discord 开发者门户创建 Bot 应用,获取 Token,启用 MESSAGE CONTENT INTENT 权限

  2. 脚本编写: 使用 discord.py 库编写脚本,核心逻辑如下:

  • 初始化客户端,监听 on_message 事件
  • 事件触发时,提取关键字段:message.content, message.author, message.channel.name
  • 迭代 message.attachments 列表,提取每个附件的 urlfilename
  • 构造为 JSON
  • 通过 requests.post() 发送至 n8n Webhook

1.3. 网络连接与代理配置

运行脚本时,遇到了 ConnectionTimeoutError,从流程来看,大概率是服务器无法连接到 Discord API,添加代理解决

import discord
import requests
import os

BOT_TOKEN = "YOUR_DISCORD_BOT_TOKEN"
N8N_WEBHOOK_URL = "YOUR_N8N_WEBHOOK_URL"
PROXY_URL = "http://your.proxy.address:port" 

client = discord.Client(
    intents=discord.Intents.default(),
    proxy=PROXY_URL if PROXY_URL else None # 应用代理
)

@client.event
async def on_message(message):
    if message.author == client.user:
        return

    attachments_data = []
    if message.attachments:
        for attachment in message.attachments:
            attachments_data.append({
                "filename": attachment.filename,
                "url": attachment.url
            })

    payload = {
        "content": message.content,
        "author": message.author.name,
        "channel_name": message.channel.name,
        "attachments": attachments_data
    }

    try:
        requests.post(N8N_WEBHOOK_URL, json=payload)
    except requests.exceptions.RequestException as e:
        print(f"Error sending data to n8n: {e}")

client.run(BOT_TOKEN)

二、n8n 内部逻辑优化

n8n收到数据后需要进行处理,我原本是复用 Make.com 的逻辑,但 Split Out, IF, Merge 等节点在处理空附件等情况导致流程挂起,继续使用模块节点构建复杂逻辑并不高效。最终方案是使用 Code 节点整合所有处理逻辑,以提升工作流的健壮性和可维护性

2.1. 统一逻辑的 Code 节点

Code 节点接收来自 Webhook 的原始 JSON:

  1. 检查附件: 判断 body.attachments 数组是否存在且长度大于零
  2. 格式化内容: 如果存在附件,将其 URL 转换为 Markdown 图片格式 ![]() M;否则生成空字符串。将格式化后的附件字符串与原始文本内容拼接
  3. 动态路径: 根据 body.channel_name 字段的值,映射到对应的目标文件名(如 thoughts.md
  4. 输出结构: 返回一个包含 finalContent(完整 Markdown)、fileNametimestamp 的 JSON 对象,供后续节点使用
const originalData = $json.body;
const attachments = originalData.attachments;
const channelName = originalData.channel_name;

let markdownUrls = '';
if (attachments && attachments.length > 0) {
    markdownUrls = attachments.map(att => `![](${att.url})`).join('\n\n');
}

// 动态文件名映射逻辑
let fileName;
switch (channelName) {
    case 'thoughts':
        fileName = 'thoughts.md';
        break;
    case 'ideals':
        fileName = 'ideals.md';
        break;
    default:
        fileName = 'Inbox.md';
}

const finalContent = `${originalData.content}\n\n${markdownUrls}`.trim();

return {
    json: {
        finalContent: finalContent,
        timestamp: new Date().toISOString().split('T')[0],
        fileName: fileName
    }
};

三、集成 AI 自动标签

为实现内容的自动分类,我在工作流中多加入了一个 HTTP 调用与 Code节点(使用OpenRouter)

3.1. HTTP Request 节点 (调用 AI)

此节点从 Code 获取处理后的 finalContent,向 LLM API 发送请求

  • Method: POST
  • URL: https://openrouter.ai/api/v1/chat/completions
  • Headers:
    • Authorization: Bearer YOUR_OPENROUTER_API_KEY
    • Content-Type: application/json
  • Body (JSON):
{
"model": "mistralai/mixtral-8x7b-instruct-v0.1",
"messages": [
{
"role": "system",
"content": "你是一个知识管理助手。根据用户文本提取简洁的、相关的标签,以中文逗号分隔,单行纯文本返回。不要包含任何前缀或标点。若无有效标签,返回'未分类'。"
},
{
"role": "user",
"content": ""
}
]
}

3.2. Code 节点 (合并数据)

此节点接收 AI 的响应,进行最终的 Markdown 格式化

  1. 提取标签: 从 AI 响应 ($json.choices[0].message.content) 中解析出标签字符串
  2. 获取原始数据: 从输入数据中获取 finalContent, fileName, timestamp
  3. 构建 Markdown: 将所有信息组合成带有 YAML Frontmatter 的标准 Markdown 格式
const aiResponse = $json.choices[0].message.content;
const upstreamData = $input.item.json;

const markdownContent = `---
timestamp: ${upstreamData.timestamp}
tags: ${aiResponse}
---
${upstreamData.finalContent}
`;

return {
    json: {
        finalContent: markdownContent,
        fileName: upstreamData.fileName
    }
};

四、HTTP Request 节点 (PUT 调用)

  • Method: PUT
  • URL: http://localhost:27123/vault/{{ $json.fileName }}
  • Headers:
    • Authorization: Bearer YOUR_OBSIDIAN_API_KEY
    • Content-Type: text/plain
  • Body (String): 引用上一个 Code 节点生成的完整 Markdown 内容。
  • Body Content: {{$json.finalContent}}

结语

  • 最终,我的 Discord 到 Obsidian 自动化工作流顺利上线
  • 这次搭建的体验让我认识到:低代码平台的可视化构建固然便捷,但当遇到复杂的逻辑处理时,过度依赖图形化组件反而会事倍功半。与其在多个 Split Out、IF、Merge、Set 等节点之间反复调试,不如直接在一个 Code 节点内编写不到 20 行的 JavaScript 代码。这种方法不仅大幅简化了流程,也让整个工作流更加稳定和高效

最终工作流:

Webhook (数据输入) -> Code (数据处理) -> HTTP Request (LLM调用) -> Code (最终格式化) -> HTTP Request (写入仓库)