Slack
快速开始
使用 Slack 蓝图为已有的 Flue 项目添加已验证的 HTTP 入口和应用自有的 Web API 行为。在终端或你选择的编码智能体中运行以下命令:
flue add channel slack
概述
Slack 蓝图安装 @flue/slack 和 Slack 官方的 @slack/web-api SDK,然后在源码根目录创建 channels/slack.ts。它还会更新所选智能体以将生成的线程回复工具绑定到已验证的 Slack 对话。
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 导出:
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 验证在签名验证后内部处理。
事件
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 客户端:
import { WebClient } from '@slack/web-api';
export const client = new WebClient(process.env.SLACK_BOT_TOKEN);Slack 工具
使用客户端定义应用自有的工具:
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 中解析:
'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 设置。