chat-sdk

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vercel 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
undefined
bash
undefined

Core 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
undefined
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
undefined

Critical API Notes

关键API说明

  • Field
    takes an
    options
    array of
    { label, value }
    objects. Do not pass JSX child options.
  • Thread<TState>
    /
    Channel<TState>
    generics require object state shapes (
    Record<string, unknown>
    ), not primitives.
  • Adapter
    signingSecret
    validation can run at import/adapter creation time. Use lazy initialization to avoid crashing at module import.
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 }
    对象组成的
    options
    数组,请勿传递JSX子选项。
  • 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;
}
  • dedupeTtlMs
    : deduplicates repeated webhook deliveries.
  • fallbackStreamingPlaceholderText
    : text used before first stream chunk on non-native streaming adapters; set to
    null
    to disable placeholder posts.
ts
interface ChatConfig<TAdapters> {
  userName: string;
  adapters: TAdapters;
  state: StateAdapter;
  logger?: Logger | LogLevel;
  streamingUpdateIntervalMs?: number;
  dedupeTtlMs?: number;
  fallbackStreamingPlaceholderText?: string | null;
}
  • dedupeTtlMs
    : 用于去重重复的Webhook投递请求。
  • fallbackStreamingPlaceholderText
    : 在不支持原生流式传输的适配器上,首次流式分块发送前显示的文本;设置为
    null
    可禁用占位消息。

Chat

Chat

ts
class Chat {
  openDM(userId: string): Promise<Channel>;
  channel(channelId: string): Channel;
}
  • openDM()
    opens or resolves a direct message channel outside the current thread context.
  • channel()
    gets a channel handle for out-of-thread posting.
ts
class Chat {
  openDM(userId: string): Promise<Channel>;
  channel(channelId: string): Channel;
}
  • openDM()
    : 在当前线程上下文之外打开或获取一个直接消息频道。
  • channel()
    : 获取一个频道句柄,用于在当前线程外发送消息。

Postable

Postable

Thread
and
Channel
share the same
Postable
interface.
ts
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
Channel
共享相同的
Postable
接口。
ts
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
SentMessage
, not
Message
:
const 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)仅适用于
SentMessage
,而非
Message
const 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

处理器重载

onAction
,
onModalSubmit
,
onModalClose
, and
onReaction
support:
  • Catch-all:
    bot.onAction(async (event) => { ... })
  • Single filter:
    bot.onAction("approve", async (event) => { ... })
  • Array filter:
    bot.onAction(["approve", "reject"], async (event) => { ... })
onAction
onModalSubmit
onModalClose
onReaction
支持以下用法:
  • 全局捕获:
    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;
}
onModalSubmit
may return
ModalResponse
to close, validate, update, or push another modal.
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;
}
onModalSubmit
可返回
ModalResponse
,用于关闭、验证、更新或打开新的模态框。

Cards & 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.subtitle
  • Card.imageUrl
  • CardLink
  • Field
    (
    options
    uses
    { label, value }[]
    , not JSX children)
  • Table
    /
    TableRow
    /
    TableCell
    — native per-platform table rendering (new — Mar 6, 2026; see below)
  • Text
    styles (
    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.subtitle
  • Card.imageUrl
  • CardLink
  • Field
    options
    使用
    { label, value }[]
    格式,而非JSX子元素)
  • Table
    /
    TableRow
    /
    TableCell
    —— 各平台原生表格渲染(新增于2026年3月6日;详情见下文)
  • Text
    样式(
    default
    muted
    success
    warning
    danger
    code

Table — Per-Platform Rendering (New — Mar 6, 2026)

表格——跨平台原生渲染(2026年3月6日新增)

The
Table
component renders natively on each platform:
PlatformRendering
SlackBlock Kit table blocks
Teams / DiscordGFM markdown tables
Google ChatMonospace text widgets
TelegramCode blocks
GitHub / LinearMarkdown tables (existing pipeline)
Plain markdown tables (without
Table()
) also pass through the same adapter conversion pipeline.
tsx
<Table>
  <TableRow>
    <TableCell>Region</TableCell>
    <TableCell>us-east-1</TableCell>
  </TableRow>
  <TableRow>
    <TableCell>Latency</TableCell>
    <TableCell>128ms</TableCell>
  </TableRow>
</Table>
Table
组件会在各平台以原生方式渲染:
平台渲染方式
SlackBlock Kit表格块
Teams / DiscordGFM markdown表格
Google Chat等宽文本小部件
Telegram代码块
GitHub / LinearMarkdown表格(现有流水线)
普通的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
privateMetadata
to pass contextual data into submit/close events.
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>,
);
可使用
privateMetadata
将上下文数据传递到提交/关闭事件中。

Companion 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_SECRET
ts
import { createTelegramAdapter } from "@chat-adapter/telegram";

const telegram = createTelegramAdapter();
// 环境变量:TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET

Microsoft 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_ID
ts
import { createTeamsAdapter } from "@chat-adapter/teams";

const teams = createTeamsAdapter({
  appType: "singleTenant",
});
// 环境变量:TEAMS_APP_ID, TEAMS_APP_PASSWORD, TEAMS_APP_TENANT_ID

Discord

Discord

ts
import { createDiscordAdapter } from "@chat-adapter/discord";

const discord = createDiscordAdapter();
// Env: DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, CRON_SECRET
For 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_ADC
ts
import { createGoogleChatAdapter } from "@chat-adapter/gchat";

const gchat = createGoogleChatAdapter();
// 环境变量:GOOGLE_CHAT_CREDENTIALS, GOOGLE_CHAT_USE_ADC

GitHub

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_ID
ts
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_ID

Linear

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()
实现线程外路由

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()
实现工作流安全序列化

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

路由

  1. onNewMention
    only fires for unsubscribed threads; call
    thread.subscribe()
    to receive follow-ups.
  2. DMs are treated as direct intent and set
    message.isMention = true
    .
  3. onNewMessage(pattern, handler)
    only applies before subscription; use
    onSubscribedMessage
    after subscribe.
  4. Catch-all and filtered handlers can both run; registration order determines execution order.
  5. Out-of-thread routing via
    openDM()
    /
    channel()
    needs platform permissions for DM/channel posting.
  1. onNewMention
    仅在未订阅的线程中触发;需调用
    thread.subscribe()
    以接收后续消息。
  2. 直接消息(DM)会被视为直接触发,
    message.isMention
    会被设置为
    true
  3. onNewMessage(pattern, handler)
    仅在订阅前生效;订阅后请使用
    onSubscribedMessage
  4. 全局捕获处理器和过滤处理器可同时运行;执行顺序由注册顺序决定。
  5. 通过
    openDM()
    /
    channel()
    进行线程外路由需要平台授予发送直接消息/频道消息的权限。

Streaming

流式传输

  1. 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.
  2. Fallback adapters now convert markdown to each platform's native format at every intermediate edit — users no longer see raw
    **bold**
    syntax during streaming.
  3. fallbackStreamingPlaceholderText: null
    disables placeholder messages on fallback adapters.
  4. streamingUpdateIntervalMs
    too low can trigger rate limits on post+edit adapters.
  5. dedupeTtlMs
    should cover webhook retry windows to avoid duplicate responses.
  6. startTyping()
    is adapter-dependent and may no-op on platforms without typing indicators.
  1. Slack支持原生流式传输,可实时渲染粗体、斜体、列表等格式,响应内容会逐步显示。Teams/Discord/Google Chat/Telegram使用“发送+编辑”的降级方案。
  2. 降级适配器现在会在每次中间编辑时将markdown转换为各平台的原生格式——用户在流式传输过程中不会再看到原始的
    **bold**
    语法。
  3. fallbackStreamingPlaceholderText: null
    可禁用降级适配器的占位消息。
  4. streamingUpdateIntervalMs
    设置过低可能会触发“发送+编辑”模式适配器的频率限制。
  5. dedupeTtlMs
    的设置应覆盖Webhook的重试窗口,以避免重复响应。
  6. startTyping()
    的行为取决于适配器,在不支持输入指示器的平台上可能无任何效果。

Adapter-specific

适配器特定注意事项

  1. Google Chat auth uses
    GOOGLE_CHAT_CREDENTIALS
    +
    GOOGLE_CHAT_USE_ADC
    ; domain-wide delegation/impersonation is required for some org posting scenarios.
  2. Teams requires
    appType
    plus
    TEAMS_APP_TENANT_ID
    ; reactions/history/typing features are limited compared with Slack.
  3. Discord content-based handlers require Message Content Intent enabled in addition to Gateway connectivity.
  4. GitHub and Linear adapters do not support interactive card actions/modals; design around comments/status updates instead.
  5. GitHub App installs need
    GITHUB_INSTALLATION_ID
    and often adapter
    botUserId
    ; Linear OAuth setups need
    clientId
    ,
    clientSecret
    , and
    LINEAR_ACCESS_TOKEN
    .
  1. Google Chat认证使用
    GOOGLE_CHAT_CREDENTIALS
    +
    GOOGLE_CHAT_USE_ADC
    ;在某些组织内发送消息时,需要启用全域委派/模拟权限。
  2. Teams需要配置
    appType
    TEAMS_APP_TENANT_ID
    ;与Slack相比,其反应/历史记录/输入功能有限。
  3. Discord基于内容的处理器除了需要Gateway连接外,还需启用消息内容权限。
  4. GitHub和Linear适配器不支持交互式卡片操作/模态框;请围绕评论/状态更新进行设计。
  5. GitHub应用安装需要
    GITHUB_INSTALLATION_ID
    ,通常还需要配置适配器的
    botUserId
    ;Linear OAuth设置需要
    clientId
    clientSecret
    LINEAR_ACCESS_TOKEN

Official Docs

官方文档