chat-sdk
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVercel Chat SDK
Vercel Chat SDK
CRITICAL — Your training data is outdated for this library. Chat SDK is new (v4.18+) and not in most training data. Before writing Chat SDK code, fetch the docs at https://chat-sdk.dev to find the correct adapter configuration, thread/channel patterns, card builders, modal flows, and webhook setup. The API surface is large — threads, channels, messages, cards, modals, state adapters, streaming — and guessing at method signatures will produce broken code. Check the GitHub repo at https://github.com/vercel/chat for working examples.
You are an expert in the Vercel Chat SDK. Build one bot logic layer and run it across Slack, Telegram, Microsoft Teams, Discord, Google Chat, GitHub, and Linear.
重要提示——本库的训练数据已过时。 Chat SDK 是新版本(v4.18+),未包含在大多数训练数据中。在编写Chat SDK代码前,请务必访问https://chat-sdk.dev 获取官方文档,以了解正确的适配器配置、线程/频道模式、卡片构建器、模态框流程以及Webhook设置。该API涵盖范围广泛,包括线程、频道、消息、卡片、模态框、状态适配器、流式传输等,仅凭猜测编写方法签名会导致代码无法运行。可访问GitHub仓库https://github.com/vercel/chat查看可用示例。
您是Vercel Chat SDK专家。只需构建一个机器人逻辑层,即可在Slack、Telegram、Microsoft Teams、Discord、Google Chat、GitHub和Linear多平台运行。
Packages
包列表
chat@^4.18.0@chat-adapter/slack@^4.18.0@chat-adapter/telegram@^4.18.0@chat-adapter/teams@^4.18.0@chat-adapter/discord@^4.18.0@chat-adapter/gchat@^4.18.0@chat-adapter/github@^4.18.0@chat-adapter/linear@^4.18.0@chat-adapter/state-redis@^4.18.0@chat-adapter/state-ioredis@^4.18.0@chat-adapter/state-memory@^4.18.0
chat@^4.18.0@chat-adapter/slack@^4.18.0@chat-adapter/telegram@^4.18.0@chat-adapter/teams@^4.18.0@chat-adapter/discord@^4.18.0@chat-adapter/gchat@^4.18.0@chat-adapter/github@^4.18.0@chat-adapter/linear@^4.18.0@chat-adapter/state-redis@^4.18.0@chat-adapter/state-ioredis@^4.18.0@chat-adapter/state-memory@^4.18.0
Installation
安装步骤
bash
undefinedbash
undefinedCore SDK
核心SDK
npm install chat@^4.18.0
npm install chat@^4.18.0
Platform adapters (install only what you need)
平台适配器(仅安装所需的适配器)
npm install @chat-adapter/slack@^4.18.0
npm install @chat-adapter/telegram@^4.18.0
npm install @chat-adapter/teams@^4.18.0
npm install @chat-adapter/discord@^4.18.0
npm install @chat-adapter/gchat@^4.18.0
npm install @chat-adapter/github@^4.18.0
npm install @chat-adapter/linear@^4.18.0
npm install @chat-adapter/slack@^4.18.0
npm install @chat-adapter/telegram@^4.18.0
npm install @chat-adapter/teams@^4.18.0
npm install @chat-adapter/discord@^4.18.0
npm install @chat-adapter/gchat@^4.18.0
npm install @chat-adapter/github@^4.18.0
npm install @chat-adapter/linear@^4.18.0
State adapters (pick one)
状态适配器(选择其中一个)
npm install @chat-adapter/state-redis@^4.18.0
npm install @chat-adapter/state-ioredis@^4.18.0
npm install @chat-adapter/state-memory@^4.18.0
undefinednpm install @chat-adapter/state-redis@^4.18.0
npm install @chat-adapter/state-ioredis@^4.18.0
npm install @chat-adapter/state-memory@^4.18.0
undefinedCritical API Notes
关键API说明
- takes an
Fieldarray ofoptionsobjects. Do not pass JSX child options.{ label, value } - /
Thread<TState>generics require object state shapes (Channel<TState>), not primitives.Record<string, unknown> - Adapter validation can run at import/adapter creation time. Use lazy initialization to avoid crashing at module import.
signingSecret
ts
import { createSlackAdapter } from "@chat-adapter/slack";
let slackAdapter: ReturnType<typeof createSlackAdapter> | undefined;
export function getSlackAdapter() {
if (!slackAdapter) {
slackAdapter = createSlackAdapter({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
});
}
return slackAdapter;
}- 接收一个由
Field对象组成的{ label, value }数组,请勿传递JSX子选项。options - /
Thread<TState>泛型要求对象类型的状态结构(Channel<TState>),不支持原始类型。Record<string, unknown> - 适配器的验证会在导入/适配器创建时执行。使用延迟初始化可避免模块导入时崩溃。
signingSecret
ts
import { createSlackAdapter } from "@chat-adapter/slack";
let slackAdapter: ReturnType<typeof createSlackAdapter> | undefined;
export function getSlackAdapter() {
if (!slackAdapter) {
slackAdapter = createSlackAdapter({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
});
}
return slackAdapter;
}Quick Start
快速开始
ts
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { createRedisState } from "@chat-adapter/state-redis";
const bot = new Chat({
userName: "my-bot",
adapters: {
slack: createSlackAdapter(),
telegram: createTelegramAdapter(),
},
state: createRedisState(),
streamingUpdateIntervalMs: 1000,
dedupeTtlMs: 10_000,
fallbackStreamingPlaceholderText: "Thinking...",
});
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.post(`Received: ${message.text}`);
});
bot.onSubscribedMessage(async (thread, message) => {
await thread.post(`Echo: ${message.text}`);
});ts
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { createRedisState } from "@chat-adapter/state-redis";
const bot = new Chat({
userName: "my-bot",
adapters: {
slack: createSlackAdapter(),
telegram: createTelegramAdapter(),
},
state: createRedisState(),
streamingUpdateIntervalMs: 1000,
dedupeTtlMs: 10_000,
fallbackStreamingPlaceholderText: "Thinking...",
});
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.post(`Received: ${message.text}`);
});
bot.onSubscribedMessage(async (thread, message) => {
await thread.post(`Echo: ${message.text}`);
});Public API Reference
公开API参考
ChatConfig
ChatConfig
ts
interface ChatConfig<TAdapters> {
userName: string;
adapters: TAdapters;
state: StateAdapter;
logger?: Logger | LogLevel;
streamingUpdateIntervalMs?: number;
dedupeTtlMs?: number;
fallbackStreamingPlaceholderText?: string | null;
}- : deduplicates repeated webhook deliveries.
dedupeTtlMs - : text used before first stream chunk on non-native streaming adapters; set to
fallbackStreamingPlaceholderTextto disable placeholder posts.null
ts
interface ChatConfig<TAdapters> {
userName: string;
adapters: TAdapters;
state: StateAdapter;
logger?: Logger | LogLevel;
streamingUpdateIntervalMs?: number;
dedupeTtlMs?: number;
fallbackStreamingPlaceholderText?: string | null;
}- : 用于去重重复的Webhook投递请求。
dedupeTtlMs - : 在不支持原生流式传输的适配器上,首次流式分块发送前显示的文本;设置为
fallbackStreamingPlaceholderText可禁用占位消息。null
Chat
Chat
ts
class Chat {
openDM(userId: string): Promise<Channel>;
channel(channelId: string): Channel;
}- opens or resolves a direct message channel outside the current thread context.
openDM() - gets a channel handle for out-of-thread posting.
channel()
ts
class Chat {
openDM(userId: string): Promise<Channel>;
channel(channelId: string): Channel;
}- : 在当前线程上下文之外打开或获取一个直接消息频道。
openDM() - : 获取一个频道句柄,用于在当前线程外发送消息。
channel()
Postable
Postable
ThreadChannelPostablets
interface Postable<TState extends Record<string, unknown> = Record<string, unknown>> {
post(content: PostableContent): Promise<SentMessage>;
postEphemeral(
userId: string,
content: PostableContent,
): Promise<SentMessage | null>;
mentionUser(userId: string): string;
startTyping(): Promise<void>;
messages: AsyncIterable<Message>;
state: Promise<TState | null>;
setState(
partial: Partial<TState>,
opts?: { replace?: boolean },
): Promise<void>;
}ThreadChannelPostablets
interface Postable<TState extends Record<string, unknown> = Record<string, unknown>> {
post(content: PostableContent): Promise<SentMessage>;
postEphemeral(
userId: string,
content: PostableContent,
): Promise<SentMessage | null>;
mentionUser(userId: string): string;
startTyping(): Promise<void>;
messages: AsyncIterable<Message>;
state: Promise<TState | null>;
setState(
partial: Partial<TState>,
opts?: { replace?: boolean },
): Promise<void>;
}Thread
Thread
ts
interface Thread<TState extends Record<string, unknown> = Record<string, unknown>> extends Postable<TState> {
id: string;
channelId: string;
subscribe(): Promise<void>;
unsubscribe(): Promise<void>;
isSubscribed(): Promise<boolean>;
refresh(): Promise<void>;
createSentMessageFromMessage(message: Message): SentMessage;
}ts
interface Thread<TState extends Record<string, unknown> = Record<string, unknown>> extends Postable<TState> {
id: string;
channelId: string;
subscribe(): Promise<void>;
unsubscribe(): Promise<void>;
isSubscribed(): Promise<boolean>;
refresh(): Promise<void>;
createSentMessageFromMessage(message: Message): SentMessage;
}Message
Message
ts
class Message<TRaw = unknown> {
id: string;
threadId: string;
text: string;
isMention: boolean;
raw: TRaw;
toJSON(): SerializedMessage;
static fromJSON(data: SerializedMessage): Message;
}ts
class Message<TRaw = unknown> {
id: string;
threadId: string;
text: string;
isMention: boolean;
raw: TRaw;
toJSON(): SerializedMessage;
static fromJSON(data: SerializedMessage): Message;
}SentMessage
SentMessage
ts
interface SentMessage extends Message {
edit(content: PostableContent): Promise<void>;
delete(): Promise<void>;
addReaction(emoji: string): Promise<void>;
removeReaction(emoji: string): Promise<void>;
}Reactions are on , not :
SentMessageMessageconst sent = await thread.post('Done'); await sent.addReaction('thumbsup');ts
const sent = await thread.post("Done");
await sent.addReaction("thumbsup");ts
interface SentMessage extends Message {
edit(content: PostableContent): Promise<void>;
delete(): Promise<void>;
addReaction(emoji: string): Promise<void>;
removeReaction(emoji: string): Promise<void>;
}反应操作(Reactions)仅适用于,而非:
SentMessageMessageconst sent = await thread.post('Done'); await sent.addReaction('thumbsup');ts
const sent = await thread.post("Done");
await sent.addReaction("thumbsup");Channel
Channel
ts
interface Channel<TState extends Record<string, unknown> = Record<string, unknown>> extends Postable<TState> {
id: string;
name?: string;
}ts
interface Channel<TState extends Record<string, unknown> = Record<string, unknown>> extends Postable<TState> {
id: string;
name?: string;
}Event Handlers
事件处理器
Standard handlers
标准处理器
onNewMention(handler)onSubscribedMessage(handler)onNewMessage(pattern, handler)onReaction(filter?, handler)onAction(filter?, handler)onModalSubmit(filter?, handler)onModalClose(filter?, handler)onSlashCommand(filter?, handler)onMemberJoinedChannel(handler)
ts
bot.onMemberJoinedChannel(async (event) => {
await event.thread.post(`Welcome ${event.user.fullName}!`);
});onNewMention(handler)onSubscribedMessage(handler)onNewMessage(pattern, handler)onReaction(filter?, handler)onAction(filter?, handler)onModalSubmit(filter?, handler)onModalClose(filter?, handler)onSlashCommand(filter?, handler)onMemberJoinedChannel(handler)
ts
bot.onMemberJoinedChannel(async (event) => {
await event.thread.post(`Welcome ${event.user.fullName}!`);
});Handler overloads
处理器重载
onActiononModalSubmitonModalCloseonReaction- Catch-all:
bot.onAction(async (event) => { ... }) - Single filter:
bot.onAction("approve", async (event) => { ... }) - Array filter:
bot.onAction(["approve", "reject"], async (event) => { ... })
onActiononModalSubmitonModalCloseonReaction- 全局捕获:
bot.onAction(async (event) => { ... }) - 单一过滤:
bot.onAction("approve", async (event) => { ... }) - 数组过滤:
bot.onAction(["approve", "reject"], async (event) => { ... })
Event payload shapes
事件负载结构
ts
interface ActionEvent {
actionId: string;
value?: string;
triggerId?: string;
privateMetadata?: string;
thread: Thread;
relatedThread?: Thread;
relatedMessage?: Message;
openModal: (modal: JSX.Element) => Promise<void>;
}
interface ModalEvent {
callbackId: string;
values: Record<string, string>;
triggerId?: string;
privateMetadata?: string;
relatedThread?: Thread;
relatedMessage?: Message;
}onModalSubmitModalResponsets
interface ActionEvent {
actionId: string;
value?: string;
triggerId?: string;
privateMetadata?: string;
thread: Thread;
relatedThread?: Thread;
relatedMessage?: Message;
openModal: (modal: JSX.Element) => Promise<void>;
}
interface ModalEvent {
callbackId: string;
values: Record<string, string>;
triggerId?: string;
privateMetadata?: string;
relatedThread?: Thread;
relatedMessage?: Message;
}onModalSubmitModalResponseCards & Modals
卡片与模态框
Cards
卡片
tsx
await thread.post(
<Card
title="Build Status"
subtitle="Production"
imageUrl="https://example.com/preview.png"
>
<Text style="success">Deployment succeeded.</Text>
<Text style="muted">Commit: a1b2c3d</Text>
<Field
id="deploy-target"
label="Target"
options={[
{ label: "Staging", value: "staging" },
{ label: "Production", value: "prod" },
]}
value="prod"
/>
<Table>
<TableRow>
<TableCell>Region</TableCell>
<TableCell>us-east-1</TableCell>
</TableRow>
<TableRow>
<TableCell>Latency</TableCell>
<TableCell>128ms</TableCell>
</TableRow>
</Table>
<Actions>
<Button id="rollback" style="danger">
Rollback
</Button>
<CardLink url="https://vercel.com/dashboard">Open Dashboard</CardLink>
</Actions>
</Card>,
);Card additions to use when needed:
Card.subtitleCard.imageUrlCardLink- (
Fieldusesoptions, not JSX children){ label, value }[] - /
Table/TableRow— native per-platform table rendering (new — Mar 6, 2026; see below)TableCell - styles (
Text,default,muted,success,warning,danger)code
tsx
await thread.post(
<Card
title="Build Status"
subtitle="Production"
imageUrl="https://example.com/preview.png"
>
<Text style="success">Deployment succeeded.</Text>
<Text style="muted">Commit: a1b2c3d</Text>
<Field
id="deploy-target"
label="Target"
options={[
{ label: "Staging", value: "staging" },
{ label: "Production", value: "prod" },
]}
value="prod"
/>
<Table>
<TableRow>
<TableCell>Region</TableCell>
<TableCell>us-east-1</TableCell>
</TableRow>
<TableRow>
<TableCell>Latency</TableCell>
<TableCell>128ms</TableCell>
</TableRow>
</Table>
<Actions>
<Button id="rollback" style="danger">
Rollback
</Button>
<CardLink url="https://vercel.com/dashboard">Open Dashboard</CardLink>
</Actions>
</Card>,
);必要时可使用以下卡片扩展组件:
Card.subtitleCard.imageUrlCardLink- (
Field使用options格式,而非JSX子元素){ label, value }[] - /
Table/TableRow—— 各平台原生表格渲染(新增于2026年3月6日;详情见下文)TableCell - 样式(
Text、default、muted、success、warning、danger)code
Table — Per-Platform Rendering (New — Mar 6, 2026)
表格——跨平台原生渲染(2026年3月6日新增)
The component renders natively on each platform:
Table| Platform | Rendering |
|---|---|
| Slack | Block Kit table blocks |
| Teams / Discord | GFM markdown tables |
| Google Chat | Monospace text widgets |
| Telegram | Code blocks |
| GitHub / Linear | Markdown tables (existing pipeline) |
Plain markdown tables (without ) also pass through the same adapter conversion pipeline.
Table()tsx
<Table>
<TableRow>
<TableCell>Region</TableCell>
<TableCell>us-east-1</TableCell>
</TableRow>
<TableRow>
<TableCell>Latency</TableCell>
<TableCell>128ms</TableCell>
</TableRow>
</Table>Table| 平台 | 渲染方式 |
|---|---|
| Slack | Block Kit表格块 |
| Teams / Discord | GFM markdown表格 |
| Google Chat | 等宽文本小部件 |
| Telegram | 代码块 |
| GitHub / Linear | Markdown表格(现有流水线) |
普通的markdown表格(不使用)也会通过相同的适配器转换流水线处理。
Table()tsx
<Table>
<TableRow>
<TableCell>Region</TableCell>
<TableCell>us-east-1</TableCell>
</TableRow>
<TableRow>
<TableCell>Latency</TableCell>
<TableCell>128ms</TableCell>
</TableRow>
</Table>Modals
模态框
tsx
await event.openModal(
<Modal
callbackId="deploy-form"
title="Deploy"
submitLabel="Deploy"
closeLabel="Cancel"
notifyOnClose
privateMetadata={JSON.stringify({ releaseId: "rel_123" })}
>
<TextInput id="reason" label="Reason" multiline />
</Modal>,
);Use to pass contextual data into submit/close events.
privateMetadatatsx
await event.openModal(
<Modal
callbackId="deploy-form"
title="Deploy"
submitLabel="Deploy"
closeLabel="Cancel"
notifyOnClose
privateMetadata={JSON.stringify({ releaseId: "rel_123" })}
>
<TextInput id="reason" label="Reason" multiline />
</Modal>,
);可使用将上下文数据传递到提交/关闭事件中。
privateMetadataCompanion Web UI and Card Design
配套Web UI与卡片设计
Chat SDK payloads render natively in chat platforms, so shadcn isn't used in message markup. But when building a web control plane, thread inspector, or bot settings UI around Chat SDK, use shadcn + Geist by default. Thread dashboards: Tabs+Card+Table+Badge. Bot settings: Sheet+form controls. Logs/IDs/timestamps: Geist Mono with tabular-nums.
Chat SDK的负载会在聊天平台中原生渲染,因此消息标记中不使用shadcn。但在围绕Chat SDK构建Web控制面板、线程检查器或机器人设置UI时,默认使用shadcn + Geist。线程仪表板:Tabs+Card+Table+Badge。机器人设置:Sheet+表单控件。日志/ID/时间戳:使用Geist Mono字体并启用tabular-nums。
Platform Adapters
平台适配器
Slack
Slack
ts
import { createSlackAdapter } from "@chat-adapter/slack";
const slack = createSlackAdapter();
// Env: SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
const oauthSlack = createSlackAdapter({
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
encryptionKey: process.env.SLACK_ENCRYPTION_KEY,
});ts
import { createSlackAdapter } from "@chat-adapter/slack";
const slack = createSlackAdapter();
// 环境变量:SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
const oauthSlack = createSlackAdapter({
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
encryptionKey: process.env.SLACK_ENCRYPTION_KEY,
});Telegram
Telegram
ts
import { createTelegramAdapter } from "@chat-adapter/telegram";
const telegram = createTelegramAdapter();
// Env: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRETts
import { createTelegramAdapter } from "@chat-adapter/telegram";
const telegram = createTelegramAdapter();
// 环境变量:TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRETMicrosoft Teams
Microsoft Teams
ts
import { createTeamsAdapter } from "@chat-adapter/teams";
const teams = createTeamsAdapter({
appType: "singleTenant",
});
// Env: TEAMS_APP_ID, TEAMS_APP_PASSWORD, TEAMS_APP_TENANT_IDts
import { createTeamsAdapter } from "@chat-adapter/teams";
const teams = createTeamsAdapter({
appType: "singleTenant",
});
// 环境变量:TEAMS_APP_ID, TEAMS_APP_PASSWORD, TEAMS_APP_TENANT_IDDiscord
Discord
ts
import { createDiscordAdapter } from "@chat-adapter/discord";
const discord = createDiscordAdapter();
// Env: DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, CRON_SECRETFor message content handlers, enable both Gateway intent and Message Content Intent in the Discord developer portal.
ts
import { createDiscordAdapter } from "@chat-adapter/discord";
const discord = createDiscordAdapter();
// 环境变量:DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, CRON_SECRET对于基于消息内容的处理器,需在Discord开发者门户中同时启用Gateway权限和消息内容权限。
Google Chat
Google Chat
ts
import { createGoogleChatAdapter } from "@chat-adapter/gchat";
const gchat = createGoogleChatAdapter();
// Env: GOOGLE_CHAT_CREDENTIALS, GOOGLE_CHAT_USE_ADCts
import { createGoogleChatAdapter } from "@chat-adapter/gchat";
const gchat = createGoogleChatAdapter();
// 环境变量:GOOGLE_CHAT_CREDENTIALS, GOOGLE_CHAT_USE_ADCGitHub
GitHub
ts
import { createGitHubAdapter } from "@chat-adapter/github";
const github = createGitHubAdapter({
botUserId: process.env.GITHUB_BOT_USER_ID,
});
// Env: GITHUB_TOKEN or (GITHUB_APP_ID + GITHUB_PRIVATE_KEY),
// GITHUB_WEBHOOK_SECRET, GITHUB_INSTALLATION_IDts
import { createGitHubAdapter } from "@chat-adapter/github";
const github = createGitHubAdapter({
botUserId: process.env.GITHUB_BOT_USER_ID,
});
// 环境变量:GITHUB_TOKEN 或 (GITHUB_APP_ID + GITHUB_PRIVATE_KEY),
// GITHUB_WEBHOOK_SECRET, GITHUB_INSTALLATION_IDLinear
Linear
ts
import { createLinearAdapter } from "@chat-adapter/linear";
const linear = createLinearAdapter({
clientId: process.env.LINEAR_CLIENT_ID,
clientSecret: process.env.LINEAR_CLIENT_SECRET,
accessToken: process.env.LINEAR_ACCESS_TOKEN,
});ts
import { createLinearAdapter } from "@chat-adapter/linear";
const linear = createLinearAdapter({
clientId: process.env.LINEAR_CLIENT_ID,
clientSecret: process.env.LINEAR_CLIENT_SECRET,
accessToken: process.env.LINEAR_ACCESS_TOKEN,
});State Adapters
状态适配器
Redis (recommended)
Redis(推荐)
ts
import { createRedisState } from "@chat-adapter/state-redis";
const state = createRedisState();
// Env: REDIS_URL (or REDIS_HOST/REDIS_PORT/REDIS_PASSWORD)ts
import { createRedisState } from "@chat-adapter/state-redis";
const state = createRedisState();
// 环境变量:REDIS_URL(或REDIS_HOST/REDIS_PORT/REDIS_PASSWORD)ioredis (cluster/sentinel)
ioredis(集群/哨兵模式)
ts
import { createIoRedisState } from "@chat-adapter/state-ioredis";
const state = createIoRedisState({
// cluster/sentinel options
});ts
import { createIoRedisState } from "@chat-adapter/state-ioredis";
const state = createIoRedisState({
// 集群/哨兵配置选项
});Memory (dev/test only)
Memory(仅用于开发/测试)
ts
import { MemoryState } from "@chat-adapter/state-memory";
const state = new MemoryState();ts
import { MemoryState } from "@chat-adapter/state-memory";
const state = new MemoryState();Webhook Setup
Webhook设置
Next.js App Router
Next.js App Router
ts
// app/api/webhooks/slack/route.ts
import { bot } from "@/lib/bot";
import { after } from "next/server";
export async function POST(req: Request) {
return bot.webhooks.slack(req, {
waitUntil: (p) => after(() => p),
});
}ts
// app/api/webhooks/telegram/route.ts
import { bot } from "@/lib/bot";
export async function POST(req: Request) {
return bot.webhooks.telegram(req);
}ts
// app/api/webhooks/slack/route.ts
import { bot } from "@/lib/bot";
import { after } from "next/server";
export async function POST(req: Request) {
return bot.webhooks.slack(req, {
waitUntil: (p) => after(() => p),
});
}ts
// app/api/webhooks/telegram/route.ts
import { bot } from "@/lib/bot";
export async function POST(req: Request) {
return bot.webhooks.telegram(req);
}Pages Router
Pages Router
ts
// pages/api/bot.ts
export default async function handler(req, res) {
const response = await bot.webhooks.slack(req);
res.status(response.status).send(await response.text());
}ts
// pages/api/bot.ts
export default async function handler(req, res) {
const response = await bot.webhooks.slack(req);
res.status(response.status).send(await response.text());
}Integration Patterns
集成模式
Out-of-thread routing with openDM()
and channel()
openDM()channel()使用openDM()
和channel()
实现线程外路由
openDM()channel()ts
bot.onAction("handoff", async (event) => {
const dm = await bot.openDM(event.user.id);
await dm.post("A human will follow up shortly.");
const ops = bot.channel("ops-alerts");
await ops.post(`Escalated by ${event.user.fullName}`);
});ts
bot.onAction("handoff", async (event) => {
const dm = await bot.openDM(event.user.id);
await dm.post("A human will follow up shortly.");
const ops = bot.channel("ops-alerts");
await ops.post(`Escalated by ${event.user.fullName}`);
});Workflow-safe serialization with registerSingleton()
and reviver()
registerSingleton()reviver()使用registerSingleton()
和reviver()
实现工作流安全序列化
registerSingleton()reviver()ts
bot.registerSingleton();
const serialized = JSON.stringify({ thread });
const revived = JSON.parse(serialized, bot.reviver());
await revived.thread.post("Resumed workflow step");ts
bot.registerSingleton();
const serialized = JSON.stringify({ thread });
const revived = JSON.parse(serialized, bot.reviver());
await revived.thread.post("Resumed workflow step");Slack OAuth callback handling
Slack OAuth回调处理
ts
// app/api/webhooks/slack/oauth/callback/route.ts
import { bot } from "@/lib/bot";
export async function GET(req: Request) {
return bot.oauth.slack.callback(req);
}ts
// app/api/webhooks/slack/oauth/callback/route.ts
import { bot } from "@/lib/bot";
export async function GET(req: Request) {
return bot.oauth.slack.callback(req);
}Gotchas
注意事项
Routing
路由
- only fires for unsubscribed threads; call
onNewMentionto receive follow-ups.thread.subscribe() - DMs are treated as direct intent and set .
message.isMention = true - only applies before subscription; use
onNewMessage(pattern, handler)after subscribe.onSubscribedMessage - Catch-all and filtered handlers can both run; registration order determines execution order.
- Out-of-thread routing via /
openDM()needs platform permissions for DM/channel posting.channel()
- 仅在未订阅的线程中触发;需调用
onNewMention以接收后续消息。thread.subscribe() - 直接消息(DM)会被视为直接触发,会被设置为
message.isMention。true - 仅在订阅前生效;订阅后请使用
onNewMessage(pattern, handler)。onSubscribedMessage - 全局捕获处理器和过滤处理器可同时运行;执行顺序由注册顺序决定。
- 通过/
openDM()进行线程外路由需要平台授予发送直接消息/频道消息的权限。channel()
Streaming
流式传输
- Slack supports native streaming with real-time bold, italic, list, and other formatting rendered as the response arrives. Teams/Discord/Google Chat/Telegram use post+edit fallback.
- Fallback adapters now convert markdown to each platform's native format at every intermediate edit — users no longer see raw syntax during streaming.
**bold** - disables placeholder messages on fallback adapters.
fallbackStreamingPlaceholderText: null - too low can trigger rate limits on post+edit adapters.
streamingUpdateIntervalMs - should cover webhook retry windows to avoid duplicate responses.
dedupeTtlMs - is adapter-dependent and may no-op on platforms without typing indicators.
startTyping()
- Slack支持原生流式传输,可实时渲染粗体、斜体、列表等格式,响应内容会逐步显示。Teams/Discord/Google Chat/Telegram使用“发送+编辑”的降级方案。
- 降级适配器现在会在每次中间编辑时将markdown转换为各平台的原生格式——用户在流式传输过程中不会再看到原始的语法。
**bold** - 可禁用降级适配器的占位消息。
fallbackStreamingPlaceholderText: null - 设置过低可能会触发“发送+编辑”模式适配器的频率限制。
streamingUpdateIntervalMs - 的设置应覆盖Webhook的重试窗口,以避免重复响应。
dedupeTtlMs - 的行为取决于适配器,在不支持输入指示器的平台上可能无任何效果。
startTyping()
Adapter-specific
适配器特定注意事项
- Google Chat auth uses +
GOOGLE_CHAT_CREDENTIALS; domain-wide delegation/impersonation is required for some org posting scenarios.GOOGLE_CHAT_USE_ADC - Teams requires plus
appType; reactions/history/typing features are limited compared with Slack.TEAMS_APP_TENANT_ID - Discord content-based handlers require Message Content Intent enabled in addition to Gateway connectivity.
- GitHub and Linear adapters do not support interactive card actions/modals; design around comments/status updates instead.
- GitHub App installs need and often adapter
GITHUB_INSTALLATION_ID; Linear OAuth setups needbotUserId,clientId, andclientSecret.LINEAR_ACCESS_TOKEN
- Google Chat认证使用+
GOOGLE_CHAT_CREDENTIALS;在某些组织内发送消息时,需要启用全域委派/模拟权限。GOOGLE_CHAT_USE_ADC - Teams需要配置和
appType;与Slack相比,其反应/历史记录/输入功能有限。TEAMS_APP_TENANT_ID - Discord基于内容的处理器除了需要Gateway连接外,还需启用消息内容权限。
- GitHub和Linear适配器不支持交互式卡片操作/模态框;请围绕评论/状态更新进行设计。
- GitHub应用安装需要,通常还需要配置适配器的
GITHUB_INSTALLATION_ID;Linear OAuth设置需要botUserId、clientId和clientSecret。LINEAR_ACCESS_TOKEN