跳转到内容

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

Slack

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

快速开始

使用 Slack 蓝图为已有的 Flue 项目添加已验证的 HTTP 入口和应用自有的 Web API 行为。在终端或你选择的编码智能体中运行以下命令:

flue add channel slack

概述

Slack 蓝图安装 @flue/slack 和 Slack 官方的 @slack/web-api SDK,然后在源码根目录创建 channels/slack.ts。它还会更新所选智能体以将生成的线程回复工具绑定到已验证的 Slack 对话。

src/channels/slack.ts(简略版)
import { dispatch } from '@flue/runtime';
import { createSlackChannel } from '@flue/slack';
import { WebClient } from '@slack/web-api';
import { Assistant } from '../agents/assistant.ts';

export const client = new WebClient(process.env.SLACK_BOT_TOKEN);

export const channel = createSlackChannel({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,
  async events({ payload }) {
    if (payload.type !== 'event_callback') return;
    if (payload.event.type !== 'app_mention') return;

    const event = payload.event;
    await dispatch(Assistant, {
      id: channel.instanceId({
        teamId: payload.team_id,
        channelId: event.channel,
        threadTs: event.thread_ts ?? event.ts,
      }),
      message: {
        kind: 'signal',
        type: 'slack.app_mention',
        body: event.text,
        attributes: { eventId: payload.event_id },
      },
    });
  },
});

简略版示例省略了生成的 replyInThread() 工具。完整蓝图在智能体模块中绑定该工具,因此已验证的应用提及会到达线程范围的智能体实例,且回复会返回到同一线程。交互性和斜杠命令回调是可选的次要添加:每个回调仅在启用时才发布其对应的路由。

挂载渠道

渠道仅在 app.ts 挂载它的地方提供 HTTP 路由。挂载模块的命名 channel 导出:

src/app.ts
import { channel as slack } from './channels/slack.ts';

app.route('/channels/slack', slack.route());

channel.route() 是一个纯路由器工厂,相对于挂载路径提供渠道声明的路由。本指南中的 webhook 路径假定使用常规的 /channels/slack 挂载路径;不同的挂载路径会相应调整。分发目标智能体模块携带 'use agent' 指令——该指令会注册它,因此仅用于分发的智能体不需要自己的 HTTP 挂载。

配置

变量用途
SLACK_SIGNING_SECRET必需 — 验证入站请求字节。
SLACK_BOT_TOKEN必需 — 验证出站 Slack Web API 调用。

蓝图安装并配置 @flue/slack 用于入站请求,以及 Slack 官方的 @slack/web-api SDK 用于出站 API 调用。运行命令后,你将获得一个新的 src/channels/slack.ts 渠道,其 webhook 路由在 app.ts 挂载 channel.route() 的地方提供服务——通常在 /channels/slack/*。

支持的 Webhook

Slack 界面Webhook 路径
事件订阅/channels/slack/events
交互性/channels/slack/interactions
斜杠命令/channels/slack/commands

仅添加你的应用处理的 Slack 界面。

从 createSlackChannel() 中省略回调会省略其路由。Slack URL 验证在签名验证后内部处理。

事件

src/channels/slack.ts
import { dispatch } from '@flue/runtime';
import { createSlackChannel } from '@flue/slack';
import { Assistant } from '../agents/assistant.ts';

export const channel = createSlackChannel({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,

  // 路径:/channels/slack/events
  async events({ payload }) {
    if (payload.type !== 'event_callback') return;

    switch (payload.event.type) {
      case 'app_mention': {
        const event = payload.event;
        const thread = {
          teamId: payload.team_id,
          channelId: event.channel,
          threadTs: event.thread_ts ?? event.ts,
        };
        await dispatch(Assistant, {
          id: channel.instanceId(thread),
          // 此事件创建实例时记录一次;之后忽略。
          initialData: {
            channelId: thread.channelId,
            threadTs: thread.threadTs,
            startedBy: event.user,
            startedAt: new Date(Number(event.ts) * 1000).toISOString(),
          },
          message: {
            kind: 'signal',
            type: 'slack.app_mention',
            body: event.text,
            attributes: { eventId: payload.event_id },
          },
        });
        return;
      }
      default:
        return;
    }
  },
});

payload 是 Slack 的外部 Events API 交付。对于 event_callback,payload.event 使用来自 @slack/types 的官方 SlackEvent 联合类型。对 payload.event.type 进行分支处理可以缩小事件范围,如 app_mention、reaction_added、Assistant 事件和 message。消息子类型仍可通过 payload.event.subtype 获取。

渠道不过滤机器人消息、消息子类型或事件族。你的处理程序决定哪些已认证的事件会影响应用。app_rate_limited 通知也会到达回调。

签名密钥验证 Slack 应用。工作区和企业标识保留在提供商 payload 中,因此应用可以在需要时强制执行允许列表。渠道不强制使用单一工作区安装模型。

交互

仅当应用处理交互时才启用此界面:

export const channel = createSlackChannel({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,

  // 路径:/channels/slack/interactions
  async interactions({ payload }) {
    switch (payload.type) {
      case 'block_actions':
        await handleActions(payload.actions);
        return;
      case 'view_submission':
        return {
          response_action: 'errors',
          errors: { email: 'Enter a valid email address.' },
        };
      default:
        return;
    }
  },
});

交互 payload 保留 Slack 的 snake_case 线路字段。trigger_id、response_url 和视图 response_urls 是短期能力。将它们保持在即时受信任的请求处理中,而不是分发的消息、模型上下文、日志或持久会话历史中。

命令

仅当应用处理斜杠命令时才启用此界面:

export const channel = createSlackChannel({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,

  // 路径:/channels/slack/commands
  async commands({ c, payload }) {
    switch (payload.command) {
      case '/triage':
        await startTriage(payload.text);
        return c.json({ response_type: 'ephemeral', text: 'Triage started.' });
      default:
        return c.json({ response_type: 'ephemeral', text: 'Unknown command.' });
    }
  },
});

命令 payload 保留 Slack 的 snake_case 线路字段。trigger_id 和 response_url 也是短期能力,应保持在即时受信任的请求处理中。

不返回任何内容会产生一个空的 200 响应。返回兼容 JSON 的数据以获得 JSON 响应,或使用 Hono 上下文进行显式状态、头和体控制。抛出的错误通过正常的 Hono 错误处理。Slack 期望快速确认,因此及时提交持久性工作,而不是在返回之前执行慢操作。

出站

出站 Slack 行为属于导出的 SDK 客户端:

src/channels/slack.ts
import { WebClient } from '@slack/web-api';

export const client = new WebClient(process.env.SLACK_BOT_TOKEN);

Slack 工具

使用客户端定义应用自有的工具:

src/channels/slack.ts
import { defineTool } from '@flue/runtime';
import * as v from 'valibot';

export function replyInThread(ref: { channelId: string; threadTs: string }) {
  return defineTool({
    name: 'reply_in_slack_thread',
    description: '在绑定到此智能体的 Slack 线程中回复。',
    input: v.object({ text: v.pipe(v.string(), v.minLength(1)) }),
    async run({ data: { text } }) {
      const result = await client.chat.postMessage({
        channel: ref.channelId,
        thread_ts: ref.threadTs,
        text,
      });
      return { output: { channel: result.channel ?? null, ts: result.ts ?? null } };
    },
  });
}

在受信任的代码中绑定目标。data 是实例的创建数据——在上面的分发创建实例时记录一次——因此智能体使用 useInitialData() 读取结构化的线程信息,而不是从实例 ID 中解析:

src/agents/assistant.ts
'use agent';
import { useInitialData, useModel, useTool } from '@flue/runtime';
import * as v from 'valibot';
import { replyInThread } from '../channels/slack.ts';

const initialData = v.object({
  channelId: v.string(),
  threadTs: v.string(),
  startedBy: v.optional(v.string()),
  startedAt: v.pipe(v.string(), v.isoTimestamp()),
});

export function Assistant() {
  useModel('anthropic/claude-haiku-4-5');
  const data = useInitialData<v.InferOutput<typeof initialData>>();
  if (!data) throw new Error('This agent is created by the Slack channel dispatch.');
  useTool(replyInThread(data));
  const startedBy = data.startedBy ? ` by <@${data.startedBy}>` : '';
  return `Reply in the bound Slack thread when appropriate. This conversation was started${startedBy} at ${data.startedAt}.`;
}

Assistant.initialData = initialData;

channel.parseInstanceId(id) 作为逃生舱口仍然可用于仅接收 ID 而没有创建数据的路由。模型选择消息文本。它不选择任意的工作区、通道、凭据或 Web API 方法。

显示 Assistant 状态

对于 Slack Assistant 线程,直接使用 SDK:

await client.assistant.threads.setStatus({
  channel_id: channelId,
  thread_ts: threadTs,
  status: 'is thinking...',
});

这是 Slack Web API 能力,不是 @flue/slack 实现的行为。

流式回复

v8 客户端通过 Slack 的流式消息 API 暴露 chatStream():

const stream = client.chatStream({
  channel: channelId,
  thread_ts: threadTs,
  recipient_team_id: teamId,
  recipient_user_id: userId,
});

await stream.append({ markdown_text: 'First part' });
await stream.append({ markdown_text: ' and the rest.' });
await stream.stop();

该示例在 workerd 中针对假 Fetch 响应执行 chat.postMessage、assistant.threads.setStatus 和 start/append/stop 流式序列。任何测试都不联系 Slack。

处理重试

Slack 可能会重试失败或超时的 Events API 交付。从 c.req.header(...) 读取 x-slack-retry-num 和 x-slack-retry-reason。保留 payload.event_id 用于跟踪,当不允许重复入站时,在分发之前在应用自有的持久存储中声明它。

OAuth 安装存储、工作区授权、Socket Mode 和令牌轮换仍然是应用的关注点。

基于 Fetch 的 Slack Web API v8 候选版本可在 Node 和 Cloudflare Workers 中运行,使用 Flue 所需的 nodejs_compat 设置。