跳转到内容

输入关键词开始搜索文档。

Discord

最后更新 查看 Markdown 源文件@flue/discord

快速开始

通过 Discord 蓝图,将经过验证的 Discord HTTP 交互和应用自有的 Discord REST 行为添加到现有 Flue 项目中。在终端或你选择的编码智能体中运行以下命令:

flue add channel discord

概述

该蓝图安装 @flue/discord 和社区维护的 @discordjs/rest 客户端。它会在源码根目录创建一个 channels/discord.ts 模块,用于验证交互、分发支持的命令、导出项目自有的 REST 客户端和消息工具,并修改选定的智能体将该工具绑定到交互的可信目标。

src/channels/discord.ts(节选)
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:

src/app.ts
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。

交互

src/channels/discord.ts
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 行为属于导出的项目自有客户端:

src/channels/discord.ts
import { REST } from '@discordjs/rest';

export const client = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN!);

机器人令牌消息、应用命令注册、交互令牌后续操作或编辑都是 Discord REST 操作。它们不是由 @flue/discord 实现的。

Discord 工具

使用客户端定义一个应用自有的工具,在可信代码中绑定其目标:

src/channels/discord.ts
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:

src/agents/assistant.ts
'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 操作。