chat-sdk

作者: openai

Vercel Chat SDK 專家指導。用於構建跨平台聊天機器人 — Slack、Telegram、Microsoft Teams、Discord、Google Chat、GitHub、Linear — 搭配…

npx skills add https://github.com/openai/plugins --skill 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.

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

Installation

# Core SDK
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

# 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

Critical API Notes

  • 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.
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

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

ChatConfig

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.

Chat

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.

Postable

Thread and Channel share the same Postable interface.

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

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

class Message<TRaw = unknown> {
  id: string;
  threadId: string;
  text: string;
  isMention: boolean;
  raw: TRaw;

  toJSON(): SerializedMessage;
  static fromJSON(data: SerializedMessage): Message;
}

SentMessage

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');

const sent = await thread.post("Done");
await sent.addReaction("thumbsup");

Channel

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)
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) => { ... })

Event payload shapes

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.

Cards & Modals

Cards

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)

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

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.

<Table>
  <TableRow>
    <TableCell>Region</TableCell>
    <TableCell>us-east-1</TableCell>
  </TableRow>
  <TableRow>
    <TableCell>Latency</TableCell>
    <TableCell>128ms</TableCell>
  </TableRow>
</Table>

Modals

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.

Companion Web UI and Card Design

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.

Platform Adapters

Slack

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,
});

Telegram

import { createTelegramAdapter } from "@chat-adapter/telegram";

const telegram = createTelegramAdapter();
// Env: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET

Microsoft Teams

import { createTeamsAdapter } from "@chat-adapter/teams";

const teams = createTeamsAdapter({
  appType: "singleTenant",
});
// Env: TEAMS_APP_ID, TEAMS_APP_PASSWORD, TEAMS_APP_TENANT_ID

Discord

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.

Google Chat

import { createGoogleChatAdapter } from "@chat-adapter/gchat";

const gchat = createGoogleChatAdapter();
// Env: GOOGLE_CHAT_CREDENTIALS, GOOGLE_CHAT_USE_ADC

GitHub

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

Linear

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)

import { createRedisState } from "@chat-adapter/state-redis";

const state = createRedisState();
// Env: REDIS_URL (or REDIS_HOST/REDIS_PORT/REDIS_PASSWORD)

ioredis (cluster/sentinel)

import { createIoRedisState } from "@chat-adapter/state-ioredis";

const state = createIoRedisState({
  // cluster/sentinel options
});

Memory (dev/test only)

import { MemoryState } from "@chat-adapter/state-memory";

const state = new MemoryState();

Webhook Setup

Next.js App Router

// 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),
  });
}
// 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/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()

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()

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

// 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.

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.

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.

Official Docs

來自 openai 的更多技能

user-context
openai
載入或管理資料分析插件的持久性來源路由偏好設定、入門邏輯、設定進度以及語義層註冊表。
official
notion-research-documentation
openai
研究Notion內容,並將其整合為結構化的簡報、報告或附有引用的比較分析。透過精準查詢搜尋並擷取Notion頁面,然後按主題組織發現,並附上內文來源引用及參考文獻章節。根據範圍與使用者目標,從四種輸出格式(快速簡報、研究摘要、比較分析、綜合報告)中選擇。使用內建範本建立與更新Notion頁面;直接連結來源,並在新資訊到來時追蹤變更。
official
rcsb-pdb-skill
openai
提交精簡的RCSB PDB請求以獲取核心元數據、Search API查詢及FASTA下載。當用戶需要簡潔的RCSB摘要時使用;保存原始JSON或…
official
pdf
openai
PDF 讀取、建立與驗證,支援視覺化渲染與程式化生成。使用 Poppler(pdftoppm)將 PDF 頁面渲染為 PNG,以便在交付前檢查版面、間距與排版。透過 reportlab 程式化生成 PDF,確保格式可靠;使用 pdfplumber 或 pypdf 提取文字與元資料。執行品質標準:無文字裁切、元素重疊、表格破損或渲染瑕疵;僅使用 ASCII 連字號,引用格式需具可讀性。使用...
official
test-coverage-improver
openai
Improve test coverage in the OpenAI Agents JS monorepo: run `pnpm test:coverage`, inspect coverage artifacts, identify low-coverage files and branches, propose…
official
playwright
openai
終端驅動的瀏覽器自動化,具備元素快照與互動式UI工作流程。透過playwright-cli包裝腳本運作(需npx),支援無頭模式與有頭模式以進行視覺化除錯。核心流程:開啟頁面、擷取快照以取得穩定元素參照、使用參照進行互動、在導航或DOM變更後重新擷取快照。包含表單填寫、點擊、輸入、多分頁管理、螢幕截圖/PDF擷取,以及流程除錯的軌跡記錄。元素參照(例如e3、e15)...
official
ukb-topmed-phewas-skill
openai
接受rsID、GRCh37或GRCh38輸入,解析為所需的GRCh38查詢,以獲取單一變異的簡潔UKB-TOPMed PheWAS摘要。當需要…時使用。
official
code-review-context
openai
模型可見上下文
official