背景
我之前通过PipeDream + Make.com
构建了discord -> obsidian
工作流,但PipeDream
每月额度有限,在测试阶段就几乎耗尽,而Make.com
虽无明显额度限制,却对工作场景有严格要求,且Watch Channel Message
一个工作流仅能监控一个频道,所以我不得不另寻出路。经过一番周折,我又回到了 n8n
一、从 Discord 到 n8n
n8n 缺少原生的 Discord Trigger
,Action
只能选择On Demand
触发,无法实现发送新消息自动转发
解决方案:通过一个外部 Python Bot 监听 Discord 事件,主动推送数据至 n8n 的 Webhook 节点
1.1 n8n Webhook 配置
首个节点为 Webhook,生成一个唯一的 URL 作为数据入口,等待请求
1.2. Discord Bot 与 Python 脚本
-
Bot 创建: 在 Discord 开发者门户创建 Bot 应用,获取 Token,启用
MESSAGE CONTENT INTENT
权限 -
脚本编写: 使用
discord.py
库编写脚本,核心逻辑如下:
- 初始化客户端,监听
on_message
事件 - 事件触发时,提取关键字段:
message.content
,message.author
,message.channel.name
- 迭代
message.attachments
列表,提取每个附件的url
和filename
- 构造为 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:
- 检查附件: 判断
body.attachments
数组是否存在且长度大于零 - 格式化内容: 如果存在附件,将其 URL 转换为 Markdown 图片格式
![]() M
;否则生成空字符串。将格式化后的附件字符串与原始文本内容拼接 - 动态路径: 根据
body.channel_name
字段的值,映射到对应的目标文件名(如thoughts.md
) - 输出结构: 返回一个包含
finalContent
(完整 Markdown)、fileName
和timestamp
的 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 => ``).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 格式化
- 提取标签: 从 AI 响应
($json.choices[0].message.content)
中解析出标签字符串 - 获取原始数据: 从输入数据中获取
finalContent
,fileName
,timestamp
- 构建 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 (写入仓库)