Discord
快速开始
通过 Discord 蓝图,将经过验证的 Discord HTTP 交互和应用自有的 Discord REST 行为添加到现有 Flue 项目中。在终端或你选择的编码智能体中运行以下命令:
flue add channel discord
概述
该蓝图安装 @flue/discord 和社区维护的 @discordjs/rest 客户端。它会在源码根目录创建一个 channels/discord.ts 模块,用于验证交互、分发支持的命令、导出项目自有的 REST 客户端和消息工具,并修改选定的智能体将该工具绑定到交互的可信目标。
import { REST } from '@discordjs/rest';
import { createDiscordChannel, type APIInteractionResponse } from '@flue/discord';
import { dispatch } from '@flue/runtime';
import { Assistant } from '../agents/assistant.ts';
export const client = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN!);
export const channel = createDiscordChannel({
publicKey: process.env.DISCORD_PUBLIC_KEY!,
async interactions({ interaction }) {
if (interaction.type !== 2 || interaction.data.name !== 'ask') {
return {
type: 4,
data: { content: 'Unsupported interaction.', flags: 64 },
} satisfies APIInteractionResponse;
}
const destination = destinationFromInteraction(interaction);
if (!destination || destination.type === 'private') {
return {
type: 4,
data: { content: 'Unsupported interaction.', flags: 64 },
} satisfies APIInteractionResponse;
}
// `/ask` 聊天输入命令的第一个字符串选项是提示词。
const question =
interaction.data.type === 1
? interaction.data.options?.find((option) => option.type === 3)?.value
: undefined;
await dispatch(Assistant, {
id: channel.instanceId(destination),
message: {
kind: 'signal',
type: 'discord.command.ask',
body: question ?? JSON.stringify(interaction.data),
attributes: { interactionId: interaction.id, commandName: interaction.data.name },
},
});
return {
type: 4,
data: { content: 'Your request was accepted.', flags: 64 },
} satisfies APIInteractionResponse;
},
});节选示例省略了生成的 destinationFromInteraction 辅助函数和消息工具。配置完成后,ask 命令会为其 Discord 目标继续智能体实例、确认交互,并让该智能体通过绑定的 REST 工具发布消息。在 Cloudflare Workers 上,REST 包会基于 Fetch 的导出,并使用 Flue 的 nodejs_compat 设置。
挂载渠道
渠道仅在 app.ts 挂载的位置提供 HTTP 路由。挂载模块中命名导出的 channel:
import { channel as discord } from './channels/discord.ts';
app.route('/channels/discord', discord.route());channel.route() 是一个纯路由器工厂,相对于挂载路径提供渠道声明的路由。本指南中的 webhook 路径假定使用常规的 /channels/discord 挂载;不同的挂载路径会相应调整。分发目标智能体模块携带 'use agent' 指令——该指令负责注册,因此仅用于分发的智能体不需要自己的 HTTP 挂载。
配置
| 变量 | 用途 |
|---|---|
DISCORD_PUBLIC_KEY | 必需 — 验证入站交互请求字节。 |
DISCORD_BOT_TOKEN | 必需 — 认证出站 Discord REST 调用。 |
该蓝图安装并配置 @flue/discord 用于入站 HTTP 交互,以及项目自有的 @discordjs/rest 客户端用于出站 API 调用。运行命令后,你将获得一个新的源码根目录 channels/discord.ts 模块,导出 channel 和 client。
Discord 没有发布官方的 JavaScript REST SDK。该蓝图使用社区维护的 @discordjs/rest 客户端。你的应用拥有该客户端及其出站 API 调用;@flue/discord 仅处理经过验证的入站 HTTP 交互。
在 Discord 开发者门户中,将应用的 Interactions Endpoint URL 设置为完整的公共 HTTPS 路由:
https://example.com/channels/discord/interactions
仅注册你的项目处理的应用命令。端点和命令注册是应用自有的提供者设置,而非渠道包的职责。
支持的 HTTP 交互
| Discord 界面 | Webhook 路径 |
|---|---|
| Interactions | /channels/discord/interactions |
Discord 可以通过 Gateway 或出站 webhook 传递交互,但同一应用不能同时使用两者。@flue/discord 实现了经过验证的 HTTP 路径。Discord Gateway 是一种持久 WebSocket 传输,不在渠道模型范围内。
签名的 PING 请求会在应用代码运行之前在内部响应 PONG。
交互
import { type APIInteractionResponse, createDiscordChannel } from '@flue/discord';
export const channel = createDiscordChannel({
publicKey: process.env.DISCORD_PUBLIC_KEY!,
// 路径:/channels/discord/interactions
async interactions({ interaction }) {
if (interaction.type === 4) {
return {
type: 8,
data: { choices: [] },
} satisfies APIInteractionResponse;
}
if (interaction.type === 2 && interaction.data.name === 'ask') {
return {
type: 4,
data: { content: 'Your request was accepted.', flags: 64 },
} satisfies APIInteractionResponse;
}
return {
type: 4,
data: { content: 'Unsupported interaction.', flags: 64 },
} satisfies APIInteractionResponse;
},
});interaction 是 Discord 提供者原生的 API v10 对象。其数字 type 判别式可以区分命令、自动补全请求、消息组件和模态提交,同时保留 Discord 的 snake_case 字段和嵌套结构。该包不过滤已认证的交互类型;由处理程序决定哪些交互会影响应用。
回调使用当前的 APIInteraction 联合类型进行强类型缩窄。已认证的未来数字类型在运行时仍会被转发,因此穷尽分支在 Discord API 变更后应能容忍未知的数字值。
在 Discord 截止时间内响应
每个非 PING 的 HTTP 交互都需要一个有效的 Discord 交互响应。如果初始响应未在三秒内发送,Discord 会使交互令牌失效。该包等待应用处理程序完成,不施加单独的超时,因此要及时承认持久工作并在该提供者截止时间内返回。
即时消息响应使用回调类型 4。当应用将通过 Discord 的 webhook API 完成交互时,延迟响应使用类型 5。交互令牌在最多 15 分钟内对后续操作保持有效。
interaction.token 是一种短期响应能力。仅在即时可信的应用代码中使用它。不要将其放入分发的消息、模型上下文、日志和持久会话历史中。
参阅 Discord 的交互回调文档了解每种交互类型允许的响应类型。
选择会话目标
并非每个交互都代表一个持久的 Discord 频道会话。当交互需要继续一个智能体实例时,应用代码可以从原生的 guild_id、channel.id、channel.type 和 context 字段派生出 DiscordDestinationRef。通过 flue add channel discord 生成的完整示例展示了该派生过程,并使用 channel.instanceId(ref) 进行分发。
一些有效的交互(包括模态提交)可能不包含频道。私有频道交互可以通过其交互令牌确认,但该能力不授予机器人对任意频道消息的访问权限。
当 Discord 目标需要继续同一智能体实例时,使用 channel.instanceId(ref)。实例 ID 是标识符,不是授权能力。参阅共享的渠道指南了解分发、授权和去重的指导。
出站 REST
出站 Discord 行为属于导出的项目自有客户端:
import { REST } from '@discordjs/rest';
export const client = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN!);机器人令牌消息、应用命令注册、交互令牌后续操作或编辑都是 Discord REST 操作。它们不是由 @flue/discord 实现的。
Discord 工具
使用客户端定义一个应用自有的工具,在可信代码中绑定其目标:
import { defineTool } from '@flue/runtime';
import * as v from 'valibot';
export function postMessage(ref: { channelId: string }) {
return defineTool({
name: 'post_discord_message',
description: '发布到绑定到此智能体的 Discord 目标。',
input: v.object({ content: v.pipe(v.string(), v.minLength(1)) }),
async run({ data: { content } }) {
const result = (await client.post(`/channels/${ref.channelId}/messages`, {
body: { content },
})) as { id?: string };
return { output: { messageId: result.id ?? null } };
},
});
}data 是实例的创建数据,在分发事件创建实例时记录一次。在创建智能体时绑定它,而不是解析实例 ID:
'use agent';
import { useInitialData, useModel, useTool } from '@flue/runtime';
import * as v from 'valibot';
import { postMessage } from '../channels/discord.ts';
const initialData = v.object({ channelId: v.string() });
export function Assistant() {
useModel('anthropic/claude-haiku-4-5');
const data = useInitialData<v.InferOutput<typeof initialData>>();
if (!data) throw new Error('此智能体由 Discord 渠道分发创建。');
useTool(postMessage(data));
return '向绑定的 Discord 目标发布简洁的回答。';
}
Assistant.initialData = initialData;模型选择消息内容。它不选择任意的 Discord 频道、凭据或 REST 方法。此工具创建普通的机器人令牌频道消息,而非交互后续操作或保证的临时响应。parseInstanceId() 作为从 ID 直接恢复目标的应急方案仍然可用。
交付和运行时行为
Discord 没有记录可靠的交互重发行为。该渠道会拒绝时间戳与服务器时钟相差超过五分钟的签名请求,这限制了重放的陈旧程度,但在其他方面它是无状态的,不会对交互 ID 进行去重。保留 interaction.id 用于追踪,在不允许重复接收时,在分发之前在应用自有的持久存储中声明它。
@flue/discord 在 Node 和 Cloudflare Workers 上运行,需要 Flue 的 nodejs_compat 设置。该示例在两种运行时中对失败关闭的假 Fetch 传输执行 @discordjs/rest 频道消息请求构造。验证你的应用所依赖的任何其他 REST 操作。