CopilotKit 发布 Channels SDK,通过统一抽象层让 AG-UI Agent 一次开发即可接入多个消息平台(Slack、Teams),内置持久化层保留记忆与上下文,并演示了通过 MCP 连接 Notion 工具的完整架构。
将 AI 智能体接入任何消息平台(如 Slack)是一件复杂的事。你需要创建 Slack 应用、处理事件、将消息格式适配平台格式、处理投递并带重试机制,以及更多。
将其投入生产环境要难得多。而如果你还想把它接入另一个消息平台,就得把大部分工作重来一遍,下一个平台又是如此。
为了消除这种重复劳动,我们兴奋地发布 Channels SDK——它能将任何 AG-UI 智能体通过一套代码库接入任何渠道,并配备持久化层,使其能够保留记忆和上下文。
今天,我们将把一个智能体接入 Slack,一路学习其架构、核心模式,以及如何通过 MCP 连接 Notion 等真实工具。
npx copilotkit@latest channels setup
想体验实时演示吗?我们在 Slack 和 MS Teams 上创建了公共频道。前往 copilotkit.ai/try-channels,我们会带你进去。
一个运行在你自有智能体上的 Slack Bot,能够:
在已有上下文的线程中回复
通过真实工具操作(通过 MCP 操作 Notion)
以原生卡片形式回复
在任何变更前请求审批
读取用户上传的文件和图片
跨对话和平台记住用户
更想要完整可用的示例?去 GitHub 克隆 OpenTag。
如果你想自己探索,可以阅读 Channels 文档,或者从文档首页复制一个现成的提示词,让你的编码智能体陪你一起构建第一个渠道。

Channels SDK 是一个开源 TypeScript 库,能够将任何 AG-UI 智能体通过一套代码库接入 Slack、Microsoft Teams、Discord、Telegram、WhatsApp 等渠道。
npm i @copilotkit/channels
它构建在 AG-UI 协议之上,因此应用和智能体保持解耦。

你可以接入任何 AG-UI 兼容的智能体框架(LangChain、Google ADK、Mastra、Pydantic AI、Claude Agent SDK 等),后续可以随时切换而无需触碰渠道代码。
如果你不想自带框架,我们还提供了一个 BuiltInAgent,支持任何模型——无论是托管的还是本地的(通过 Ollama、LM Studio 或 vLLM)。本指南将使用这个内置智能体。
你用 <Message> JSX 编写回复,每个平台会以原生方式渲染它们:Slack 上是 Block Kit,Teams 上是 Adaptive Cards。

将智能体接入你的渠道通常涉及:
这是直接路径,我们确实提供了适配器。但要运行在生产环境,我们为你提供了由 CopilotKit Intelligence 托管的 Slack 和 Microsoft Teams。
它维护连接并将每一轮对话投递给你的应用,所以你的进程只需运行智能体,无需暴露公网 URL,在仪表盘中点击即可添加平台。
本指南使用托管路径,因为这是你在生产环境中运行的路径。集成代码两种方式几乎相同。
下图展示了你的应用与 CopilotKit Intelligence 之间的边界,以及每条消息的传递路径。

这里有一个组件库,提供了构建块:Message、Section、Header、Table、Image、Chart、Actions、Button 等,帮助你轻松组合 UI 块。
把几个组合在一起,就能得到一条丰富的回复,并为你原生渲染。
import { Message, Header, Section, Context } from "@copilotkit/channels/ui";
<Message>
<Header>Deploy complete</Header>
<Section>v1.4.2 is live in production.</Section>
<Context>Shipped by the deploy bot · 12:04 PM</Context>
</Message>
你需要 Node.js 22+、一个可以安装应用的 Slack 工作区、一个免费的 CopilotKit Intelligence 密钥,以及一个模型 API 密钥(如 OpenAI)。
项目结构如下。随着我们在下面添加功能(工具、卡片、MCP),我们将在 components.tsx 中处理卡片和按钮,在 mcp.ts 中处理 Notion 客户端。其他文件保持不变。
slack-bot/
├── agent.ts # BuiltInAgent + 你的模型
├── channel.tsx # 应用
├── tsconfig.json # JSX transform 指向 @copilotkit/channels
├── package.json
└── .env # INTELLIGENCE_API_KEY, ...
在 Intelligence 仪表盘中创建一个渠道,选择平台并设置名称,如 support-slack。你的应用稍后需要声明这个确切的名称。

Intelligence 会生成一个 Slack 应用清单。在 api.slack.com/apps 从清单创建 Slack 应用,安装后并将两个值粘贴回 Intelligence:
两者缺一不可。Bot 令牌让 Intelligence 以你的应用身份发帖,签名密钥让它验证 Slack 的事件。没有这个密钥,事件永远不会投递。


创建渠道后,仪表盘看起来是这样的。

你安装的清单已经开启了这些,所以无需额外操作。了解一下它们的作用是有意义的,因为本指南后面的功能依赖它们。
互动功能已开启,这是按钮和审批功能正常工作的前提。
作用域已设置为启用记忆、读取上传文件并保留上下文。
如果你是手动构建 Slack 应用而非从清单创建,你需要在这些地方启用它们,然后在工作区重新安装 Slack 应用。

内置智能体运行在你的应用内部,所以不需要运行第二个服务器。代码如下。
// agent.ts
import { BuiltInAgent } from "@copilotkit/runtime/v2";
export function makeAgent(threadId: string) {
const agent = new BuiltInAgent({ model: "openai/gpt-5.5" });
agent.threadId = threadId;
return agent;
}
模型字符串格式为 provider/model。它开箱即用支持 OpenAI、Anthropic 和 Google,并与任何 OpenAI 兼容端点配合,因此你可以将其指向本地模型(Ollama、自托管网关)而非托管模型。本地和自定义提供商的设置请参阅 Model Selection。
初始化项目并安装 SDK,以及用于运行 TypeScript 的开发工具。
npm init -y && npm pkg set type=module
npm install @copilotkit/channels @copilotkit/runtime
npm install -D tsx typescript @types/node dotenv
Channels JSX 不使用 React。将 JSX transform 指向 @copilotkit/channels,这样 <Message> 和 <Button> 就成为类型检查过的 Slack UI,而非 React 元素。将 JSX 放在 .tsx 文件中并引入即可。
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx",
"jsxImportSource": "@copilotkit/channels",
"strict": true,
"noEmit": true
},
"include": ["*.ts", "*.tsx"]
}
注意:如果组件抛出 "React is not defined",那是因为 JSX 回退到了 React。上面的 jsxImportSource 行正是防止这种情况的关键。
在根目录创建 .env 并添加你的 API 密钥。你可以从仪表盘创建一个免费的 CopilotKit Intelligence 密钥。
INTELLIGENCE_API_KEY=cpk-…
INTELLIGENCE_CHANNEL_NAME=toothless
OPENAI_API_KEY=sk-…
PORT=3000

应用将两边联系在一起:createChannel 完成设置,onMessage 处理程序运行智能体,listener 保持进程存活。
代码工作流程如下:
createChannel 声明机器人、渠道、智能体和存储。
onMessage 对每条消息运行智能体并将回复流式传回线程。
运行时(runtime)和监听器(listener)用你的 API 密钥连接到 Intelligence 并保持进程存活。
// channel.tsx
import "dotenv/config";
import { createServer } from "node:http";
import { createChannel } from "@copilotkit/channels";
import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
import { makeAgent } from "./agent";
```typescript
const channel = createChannel({
name: process.env.INTELLIGENCE_CHANNEL_NAME!, // 第一步中的 channel Code
identifyUser: "platform",
agent: makeAgent,
// 丢弃重复投递,这样 Slack 重试就不会导致智能体重复运行
store: { concurrency: "drop", lockTtl: 60_000, dedupTtl: 300_000 },
});
channel.onMessage(async ({ thread, message }) => {
await thread.runAgent({ prompt: message.text });
});
const intelligence = new CopilotKitIntelligence({ apiKey: process.env.INTELLIGENCE_API_KEY! });
const runtime = new CopilotRuntime({ agents: {}, intelligence, channels: [channel] });
const listener = createCopilotNodeListener({ runtime, basePath: "/api/copilotkit" });
await listener.channels?.ready({ timeoutMs: 30_000 });
createServer(listener).listen(Number(process.env.PORT ?? 3000));
console.log("Channel online.");
Slack 会对没有快速回复的消息进行重试,如果没有 store,那些重试就会变成重复运行。store 为每个对话保留一次运行,并丢弃重复的请求。
智能体运行在应用内部,所以一条命令就能启动一切。
node --env-file=.env --import tsx channel.tsx
如果按步骤操作完成,channel 状态应该从 "Waiting for runtime" 变为 "Online"。

把应用邀请到一个频道并 @ 它。它会在帖子线程中回复。
/invite @your-app
@your-app what can you help with?

整个设置就是这些。一边是 Slack 应用代码,另一边是你的智能体。
现在让我们通过实现生成式 UI、Human-in-the-loop、MCP 工具、线程等功能来让智能体真正有用。
Generative UI 让你的 Slack 智能体能够动态构建界面。它不是在文本中回答问题,而是根据当前需求构建内容,并发布到线程中。
原生卡片使这一切成为可能。你用 JSX 编写它们,来自 @copilotkit/channels/ui,它们渲染成真实、可交互的 Slack UI:标题、区块、按钮等。
/** @jsxImportSource @copilotkit/channels */
import { Message, Header, Section, Context } from "@copilotkit/channels/ui";
export function TaskCard({ task, owner, priority, status }: TaskRow) {
return (
<Message>
<Header>{`🔴 ${task}`}</Header>
<Section>{`*${priority}* · ${status}`}</Section>
<Context>{`👤 ${owner}`}</Context>
</Message>
);
}
原生组件覆盖的不仅是文本。除了卡片,你还可以在 Slack 中原生渲染图表、表格和图片。以下是一个图表:
/** @jsxImportSource @copilotkit/channels */
import { Chart } from "@copilotkit/channels/ui";
export function CloseTimeChart({ rows }: { rows: { area: string; avgDays: number }[] }) {
return (
<Chart
type="verticalBar"
title="Avg days to close, by area"
xAxisTitle="Area"
yAxisTitle="Days"
data={rows.map((r) => ({ label: r.area, value: r.avgDays }))}
/>
);
}
智能体从对话中填充数据,所以"按区域统计关单时间"就变成了线程中真实的柱状图。

图片用于原生组件无法绘制的场景,比如团队刚脑暴出的流程图,或一个完整样式的部件。你将它渲染成 PNG 并作为文件发布,由 Intelligence 托管。
const png = await renderFlowchart(triage); // mermaid 或 HTML → PNG
await thread.postFile({
bytes: png,
filename: "flow.png",
title: "Docs issue backlog triage flow",
altText: "Triage path from raw issue state to the main backlog risk",
});

更多内容请阅读 Rich messages 文档。
只有一个模型的智能体只能基于它已掌握的知识来回答。工具让它能调用你的代码、访问 API、执行搜索、读取数据库,并用结果来回复。
你用 defineChannelTool 来定义一个:参数 schema 成为工具的输入,当智能体调用它时你的 handler 执行。
下面是一个精简的代码片段展示这个流程——你可以使用 Tavily 或其他合适的搜索提供商。
// tools.ts
import { defineChannelTool } from "@copilotkit/channels";
import { z } from "zod";
export const webSearch = defineChannelTool({
name: "web_search",
description: "Search the web and return the top results.",
parameters: z.object({
query: z.string().describe("What to search for"),
}),
async handler({ query }) {
const results = await search(query); // 你的搜索调用,例如 Tavily
return results; // 对象会被序列化后传给模型
},
});
在 channel 上注册它:
const channel = createChannel({
name: process.env.INTELLIGENCE_CHANNEL_NAME!,
identifyUser: "platform",
agent: makeAgent,
tools: [webSearch],
});
现在,"搜索最新的 Anthropic Claude 新闻并总结"会运行 web_search,读取结果,并在线程中基于那个上下文回复。

来自 API 的一些经验法则:数据类工具返回原始对象或数组,handler 已经发了 UI 时返回一个简短的确认,抛出真正的错误以便智能体恢复,不要对你的成功数据使用 JSON.stringify。
阅读 Tools and context 文档。
defineChannelTool 适用于你自己编写的操作。如果要给智能体一整套其他工具,可以使用 MCP 服务器连接,它能搜索知识库、创建工单或更新页面。
以连接 Notion MCP 为例。
在 notion.so/profile/integrations 创建一个内部集成,复制其 token(ntn_…)。
打开一个 Notion 页面,右上角 ••• → Connections → 添加你的集成。智能体只能看到你连接的页面。

把 token 添加到 .env。
NOTION_TOKEN=ntn_...
然后将 Notion MCP 服务器接入智能体的 MCP 客户端之一。它作为本地进程运行(npx @notionhq/notion-mcp-server),并向智能体暴露 Notion 的工具:搜索、获取、创建、更新。
import { BuiltInAgent } from "@copilotkit/runtime/v2";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
StdioClientTransport,
getDefaultEnvironment,
} from "@modelcontextprotocol/sdk/client/stdio.js";
function notionMcp() {
const client = new Client({ name: "my-bot", version: "0.1.0" }, { capabilities: {} });
return {
async tools() {
await client.connect(
new StdioClientTransport({
command: "npx",
args: ["-y", "@notionhq/notion-mcp-server"],
// 转发 env,否则 NOTION_TOKEN 不会到达启动的服务器
env: { ...getDefaultEnvironment(), ...process.env } as Record<string, string>,
}),
);
const { tools } = await client.listTools();
// 将每个 MCP 工具暴露给智能体(完整映射在仓库的 mcp.ts 中)
return toToolSet(client, tools);
},
};
}
export function makeAgent(threadId: string) {
const agent = new BuiltInAgent({
model: "openai/gpt-5.5",
maxSteps: 10, // 让它调用工具、读取结果、然后回答
mcpClients: [notionMcp()],
});
agent.threadId = threadId;
return agent;
}
现在,"我的 Q3 路线图里有什么?"会搜索 Notion 并从真实页面回答。如果页面没有连接到集成,智能体会说找不到,而不是胡乱猜测。如果你希望以结构化方式回复,也可以启用生成式 UI。

这里有一个获取 Linear 工单的例子,模式类似。

有些操作不应该在没有人审批的情况下执行。对于这些关键操作,智能体可以通过人机交互(human-in-the-loop)来完成。
你可以将提示词写成一张卡片,上面有两个按钮,点击按钮后会将你的值传回处理器。
/** @jsxImportSource @copilotkit/channels */
import { Message, Header, Actions, Button, type InteractionContext } from "@copilotkit/channels/ui";
// The click runs in your runtime; the platform only sends back the button's value.
async function onDecision({ action, thread }: InteractionContext<{ ok: boolean; what: string }>) {
const v = action.value;
if (!v) return; // the click value can be undefined, so guard it first
await thread.post(v.ok ? `Approved: ${v.what}` : "Cancelled.");
if (v.ok) {
await thread.runAgent({ prompt: `The user approved "${v.what}". Do it now, then confirm.` });
}
}
export function ConfirmAction({ what }: { what: string }) {
return (
<Message>
<Header>{`Approve: ${what}`}</Header>
<Actions>
<Button value={{ ok: true, what }} style="primary" onClick={onDecision}>Approve</Button>
<Button value={{ ok: false, what }} style="danger" onClick={onDecision}>Deny</Button>
</Actions>
</Message>
);
}
告诉智能体在任何写操作之前请求审批。它会发布提示词并暂停,只有在获得批准后才继续。更多细节请参阅交互式消息文档。

你也可以使用 what 字段添加更多上下文。

读取用户上传的文件和图片
人们会在 Slack 中放入文件、截图、PDF 和日志。智能体会将消息携带的内容转发到运行环境中,附件会随文本一起传递。
channel.onMessage(async ({ thread, message }) => {
await thread.runAgent({
prompt: message.contentParts?.length
? [{ type: "text" as const, text: message.text }, ...(message.contentParts ?? [])]
: message.text,
});
});
下面是一个附加图片的示例,智能体相应地给出了回应。

Memory:对话历史与跨平台记录
智能体通过两种方式进行记忆。
对话历史是智能体对当前线程的记忆。Intelligence 在每一轮对话时都会植入近期的消息,这样在同一个线程中的后续提问就具有上下文。这不需要存储任何内容,是自动完成的。
记录(Transcripts)是 SDK 自身的记忆,按用户维度在各个平台上保留。有人在 Teams 中提问后,可以在 Slack 中继续追问,智能体仍然具有之前的上下文。
记录按用户进行键控,因此你需要为 channel 提供一个稳定的身份标识,然后在 store 中开启记录功能。
const channel = createChannel({
name: process.env.INTELLIGENCE_CHANNEL_NAME!,
identifyUser: ({ actor }) =>
actor.email ? { id: actor.email, name: actor.name ?? actor.email } : null, // needs users:read.email
agent: makeAgent,
store: {
concurrency: "drop", lockTtl: 60_000, dedupTtl: 300_000,
adapter: new MyStore(), // Redis, Postgres
transcripts: { retention: "30d", maxPerUser: 200 },
},
});
然后调用 thread.runAgent({ prompt, transcript: true }),当线程解析到用户时,之前的对话历史就会被注入。
记录和待处理的审批都存储在这个 store 中,与 Intelligence 线程保持的上下文是分开的。在开发环境中,数据存储在内存中,重启后会重置,因此生产环境需要使用 Redis 或 Postgres 等持久化存储。
太棒了!现在你的智能体已经非常强大,能够完成大量任务,并且具备你的 channel 的上下文。
在 Intelligence 仪表板中,你可以通过 AG-UI 事件查看所有线程、使用历史、运行记录以及每个线程的完整智能体生命周期。
![intelligence raw ag-ui events](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=sc