关于 copilotkit-channels
面向 CopilotKit 托管式 Intelligence Channels 的配置与定制技能,可将 Slack 或 Microsoft Teams 消息转给 Agent 运行时。内容涵盖服务商设置、运行时代码接线及连接和投递问题排查。
主要功能
- 01指导配置托管式 Slack 和 Teams
- 02说明脚手架及现有项目接线
- 03定制 Channel 处理器
- 04核对服务商、源码和服务器状态
- 05排查激活与消息投递故障
适用场景
- 01将 Agent 运行时接入 Slack 应用
- 02通过 Intelligence 配置 Teams Channel
- 03定制消息和提及处理逻辑
- 04排查 Channel 已在线但无响应的问题
MCP 相关
这是用于配置 CopilotKit Intelligence Channels 的技能;README 未说明其与 MCP 的集成。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ copilotkit/ copilotkit --skill copilotkit-channels安装后在 Claude Code 中输入/copilotkit-channels确认已加载。
附带文件sources.md
详细介绍
CopilotKit Channels
托管 Channels 可让 Agent 在 Slack 或 Microsoft Teams 中作答。Intelligence 负责管理提供商边缘层——签名入口、出口和凭据存储——并通过其实时传输将对话轮次传送到你的运行时。
范围
本技能涵盖托管的 Intelligence Channels:CopilotIntelligenceRuntime、app-api Channel 资源和实时网关。
本技能不涵盖自托管的 @copilotkit/channels 提供商适配器(@copilotkit/channels-slack、-teams、-discord、-whatsapp)。这些适配器会在你的进程中保存提供商凭据,并直接与提供商通信。两种产品都使用“channels”和“Slack”这两个词,因此在进行任何接线配置前,请先确认用户指的是哪一种。如果他们想自行保存 Slack 令牌并运行自己的入口,就需要使用适配器包,而不是本技能。
先判断你属于哪种情况
以下所有内容都取决于这一点;如果判断错误,就会导致项目中出现两个相互冲突、争用同一个 Channel 的声明。
检查项目根目录是否存在 channel-host.mts。
| 存在 | 不存在 |
|---|---|
项目由 npx copilotkit init 脚手架生成。代码部分已经完成——请转到“自定义脚手架生成的 Channel”。不要编写新的 host,也不要创建第二个 createChannel。 | 项目早于 Channel,或者不是由 CLI 创建的。请转到文末的“为 CLI 未生成的项目接线”。 |
脚手架生成的路径经过了端到端测试。为现有项目接线也受支持且可以正常工作,但这种情况较少见:CLI 会读取它未生成的源代码,因此,如果项目布局比较特殊,status 这一项可能会被报告为 undetermined,而不是给出确定的答案。这是有意为之——不确定的答案胜过错误的答案——但在这种情况下,请自行验证接线配置,不要只相信绿色报告。
Channel 分为两部分
只有两部分都正确,Channel 才能正常工作,而且彼此无法看到对方的状态:
- 提供商部分——在 Slack 或 Microsoft 注册的应用、存储在服务端的凭据,以及指向 Intelligence 的入口。它有三个入口:
npx copilotkit init会引导新项目,npx copilotkit channels add用于现有项目或 Agent,控制面板向导则可在浏览器中使用。新的 Teams 应用从通过浏览器创建的草稿开始,并使用以下两种路径之一。 - 代码部分——一个长期运行的运行时,用于声明 Channel 并等待激活。脚手架生成的项目已经包含这一部分;其他项目则需要通过本技能编写。
这个产品中最令人困惑的故障,是提供商部分配置正确,但代码部分缺失:应用正常提供 HTTP 服务,不报告任何错误,在控制面板中显示令人安心的徽章,却不会作答。浏览器中的任何信息都无法诊断这一问题,因为缺失的部分位于源代码树中。
Teams 提供商配置有两条路径
对于 Teams,请先在 Intelligence 中创建持久化的 Channel 草稿,再配置 Microsoft 资源。随后,浏览器会提供:
-
**快速 CLI 配置(推荐):**从草稿中复制完整限定命令:
npx copilotkit@latest channels add --project-id <project-id> --channel-id <channel-id> --adapter teams --provision --via skill:copilotkit-channels在复制的命令中添加
--via skill:copilotkit-channels,并在此技能的每条 CLI 命令中保留该参数。它只会告知 CopilotKit 的遥测信息,表明调用来自此技能。如果 CLI 报告Unknown option '--via',说明 npx 运行的是旧缓存版本:不带该参数重新运行命令。此方式可在仓库之外使用,复用 CopilotKit 登录状态,确认 Microsoft 租户,创建一个单租户的 Teams 托管应用,并将生成的密钥直接发送到 Intelligence 的加密存储中。它不会打印密钥、写入提供商环境文件,也不会修改运行时代码。
-
**引导式手动配置:**使用 Teams Developer Portal 和 Microsoft Entra,输入仅写入凭据,并从浏览器下载完成的程序包。常规流程不需要 Azure Bot,也绝不会要求用户编辑
manifest.json。
两条路径共用相同的持久化配置状态,并可在配置过程中互相替换。需要管理员同意 Files.ReadWrite.All,并且需要进行一次 添加到团队 安装。非管理员暂停时,结果为成功的 blocked 状态,并提供一个审批链接和相同的恢复命令;重新运行时必须复用现有的 Microsoft 应用。
自定义图标和程序包字节应留在浏览器/CLI 内存中或临时本地目录中。绝不要将它们发送到 Intelligence、Redis 或其他中继。如果尝试在上传前停止,则必须要求用户重新选择自定义图标(或明确选择默认风筝图标),并在退出时删除所有临时副本。
对 已创建并安装 应作狭义理解:Microsoft 应用已存在,加密凭据已存储,文件和 Team 消息访问权限已获批准,并且已记录 Team 安装。这并不能证明此运行时已上线或消息已送达。
自定义脚手架生成的 Channel
脚手架生成的项目包含三个文件,其中只有一个文件需要你修改:
| 文件 | 说明 | 是否修改? |
|---|---|---|
channels.mts | Channel:名称解析、Agent 和处理器 | 是 — 在这里进行自定义 |
channel-host.mts | 负责 Channel 生命周期的进程 | 否。所有入门模板和所有提供商使用的内容都相同 |
src/agent.ts (or app/agent.ts) | Web 路由和 Channel 共用的 Agent | 仅在需要修改 Agent 本身时 |
运行方式:
npm run channel
网关连接正常时会打印 Channel "<name>" is online;如果提供商部分尚未完成,则会打印 declared but no provider is attached yet——这表示正在等待,而不是错误。
修改什么,以及在哪里修改
所有修改都在 channels.mts 的 createDefaultChannel 内进行,具体是在返回 channel 对象之前:
// Reply to a thread the bot is mentioned in, in addition to the messages it
// already handles. Registering onMention does NOT replace onMessage: a
// non-mention turn is only ever dispatched to message handlers, so removing
// onMessage makes the bot silent in DMs and on 1:1 platforms.
channel.onMention(async ({ thread, message }) => {
/* ... */
});
// Act on a reaction. `added` distinguishes an added emoji from a removed one,
// and `thread` is the conversation it happened in.
channel.onReaction(async ({ thread, emoji, added }) => {
/* ... */
});
此列表中有意不包含 onCommand。托管 Slack Channel 仅支持事件——没有托管的斜杠命令入口——因此注册该处理器后,永远不会有任何内容调用它。
脚手架有意省略了各提供商专属的工具和上下文(defaultSlackTools、defaultSlackContext),并非疏忽:导入它们会使文件依赖特定提供商,而脚手架生成的 Channel 并非如此。如果项目只面向一个提供商,请在此处添加它们。
不要对脚手架项目做的两件事
- 不要添加第二个
createChannel。 宿主会从.copilotkit/channels.json中解析出恰好一个 Channel 名称,当声明了多个时会拒绝启动。在源码中再声明一个并不会产生第二个机器人;它会产生一个无法启动的项目。 - 不要把 Channel 移到 Next.js 路由中。 该路由是无服务器的,无法保持连接打开。独立宿主的存在正是出于这个原因。
验证 —— 两条路径皆可
npx copilotkit channels status --via skill:copilotkit-channels
它会比较三件必须一致的内容 —— 声明的配置、项目源码和服务 —— 并指出缺失的是哪一个。然后:
- 启动长时间运行的宿主 —— 在脚手架项目中执行
npm run channel。它应当记录激活信息。 - 在 Slack 中,将应用邀请到某个频道(
channels status会打印出带有正确句柄的邀请命令)。在 Teams 中,提供方设置必须已经记录了 Add to a team;不要用仅限个人的安装来替代。 - 发送一次真实的提及或私信。你应当收到回复。
如果什么都没发生,也没有错误,请按此顺序检查:是否在某处设置了 activateChannels: false;在延迟挂载时,是否 await 了 ready();传入的是否是 intelligence(而不是 runner);实时 URL 是否正确;在 Slack 中,应用在创建后是否被重新安装且机器人已被邀请。
"重新安装" 不是笔误。Slack 在创建应用时会安装它,但只授予其中两个 scope,只有重新安装才会授予其余的 —— 见下文 "在线、静默,日志中什么都没有"。
不要因为凭据已存储就报告某个 Channel 正常工作。已存储的适配器只能证明凭据是真实的 —— 服务探测过它们 —— 仅此而已。它不能证明应用已安装、频道已被邀请,或运行时已连接。
在线但静默 —— 另一种不同的故障
该清单只覆盖从未连接过的 Channel。如果宿主记录 Channel "<name>" is online 而机器人仍然什么都不说,那么代码那一半是没问题的 —— 激活成功了 —— 上面每一项复查都会是正确的。再走一遍清单只是浪费时间。
在宿主自身的输出中查找:
{ meta: { deliveryId: 'dlv_...', errorCategory: 'validation' } } channel delivery claim or join failed
这是运行时在加入边界处、任何处理器运行之前,拒绝了服务发送给它的该轮请求。在这条路径上,不会有任何内容回传给提供方,这就是为什么它看起来像是静默而非错误。
常见原因是已安装的 @copilotkit/channels-* 包与为其提供服务的 Intelligence 部署之间存在版本不一致。运行时会严格校验每一次投递 —— 精确的字段集合,而非宽松的子集 —— 因此,如果客户端期望服务尚未发送的某个字段,这类轮次都会失败,而一个从未收到该字段的 Channel(仅 Teams,或空闲的 Slack 应用)则看起来完全健康。
所以:
- 记下已安装的版本:
npm ls @copilotkit/runtime @copilotkit/channels。 - 将它们与 Intelligence 部署进行比较。在托管 Intelligence 上,canary 或预发布客户端可能领先于已部署的版本。在自托管的情况下,通常是部署版本落后。
- 让它们位于匹配的版本线上。将客户端固定到预发布版本是进入此状态的常见途径。
errorCategory 是一种分类,而不是消息本身 —— 底层错误文本被有意不记录日志,因此该分类加上这条说明就是你能得到的全部信号。
在线、静默,而且日志中毫无记录 — 范围过窄的 Slack 令牌
如果主机日志显示在线,机器人却毫无反应,且输出中完全没有任何投递记录,说明这一轮根本没有到达你这里:Slack 从未发送它。在 Slack 上,这几乎总是因为机器人令牌复制得太早。
使用清单创建 Slack 应用时,应用会安装,且该安装只授予两个权限范围 — channels:history 和 chat:write。清单中声明的权限范围会进入应用配置,却不会进入授权范围;Slack 黄色的“你已更改权限范围”横幅报告的正是这一点。点击一次 Reinstall to Workspace → Allow,就会将授权范围扩展至完整集合。
在重新安装之前复制令牌就是陷阱,因为所有检查仍会通过:
auth.test成功,因此绑定时会存储该令牌,并报告适配器运行正常。chat:write已存在,因此机器人可以发帖 — 它看起来并没有明显故障。- 缺少
app_mentions:read,因此 Slack 永远不会投递app_mention,也不会运行任何处理程序。
结果就是一个确实在线、但从结构上无法听见消息的 Channel。只要知道该看哪里,就很容易将它与上文提到的版本不一致区分开:那种故障会记录被拒绝的投递,而这种故障什么都不会记录,因为没有任何内容可供拒绝。
修复方法是重新安装 Slack 应用,复制重新签发的 Bot User OAuth Token — 重新安装会签发新令牌 — 然后轮换已存储的凭据(npx copilotkit channels rotate <name> --adapter slack --via skill:copilotkit-channels)。
现在,Intelligence 在粘贴权限范围过窄的令牌时会拒绝它,并报告 CHANNEL_ADAPTER_SLACK_TOKEN_SCOPES_INCOMPLETE,同时指出缺少哪些权限范围。应将此错误视为提前发现了这个问题,而不是配置失败。在这项检查推出之前绑定的 Channel 仍可能处于此状态,只有轮换才能清除它。
如果重新安装后问题仍未解决,说明该应用早于当前清单,其存储的配置也已过时:将当前清单粘贴到 Slack 应用中的 App Manifest,然后重新安装。
手动配置 CLI 未生成的项目
从这里往下介绍的都是手动接线方式 — 这是较少使用的路径。如果存在 channel-host.mts,说明你进入了错误的章节。
先决条件
脚手架生成的项目已经满足以下全部四项。开始之前,请确认四项都满足。任何一项不满足都应先停止并修复 — 每项都会导致静默失败,而不是报错。
- 已接入 Intelligence 运行时。 SSE 模式下无法使用
channels;此时其类型为channels?: undefined。如果项目仍使用runner而没有intelligence来构建CopilotRuntime,请先将项目连接到 Intelligence — 按照 copilotkit-cli 技能中的说明运行copilotkit login,然后运行copilotkit project select。 - 长时间运行的主机。 激活会打开持久连接,因此进程必须持续运行,不能随请求结束。Next.js 的无服务器路由处理程序、Lambda 或边缘函数都无法托管 Channel。请参阅下文的“部署形态”。
- 已设置托管环境变量 — 项目 API 密钥,以及覆盖默认值时使用的实时 URL。
- 提供方部分已就绪或正在配置中。 两个部分可以按任意顺序完成;只有两者都完成后,Channel 才会应答。
步骤 1:安装
npm install @copilotkit/channels
@copilotkit/channels 提供 createChannel。托管传输本身内置于 @copilotkit/runtime — 托管路径无需安装单独的适配器包,进程中也无需提供方凭据。
第 2 步:声明 Channel
Channel 的 name 在这里、在代码中选定。它是项目唯一的标识符,运行时用它来推导出所管理 Channel 的激活配置 —— 项目 id、适配器、socket URL 和认证 —— 因此这些你都不需要提供。
import { createChannel } from "@copilotkit/channels";
const support = createChannel({
// Must match the Channel name configured on the provider half.
// Lowercase kebab-case, unique within the project.
name: "support",
agent: (threadId) => {
const agent = new MyAgent();
agent.threadId = threadId;
return agent;
},
});
support.onMention(async ({ thread, message }) => {
await thread.runAgent({
// Channel history does NOT include the in-flight turn, so pass the current
// message explicitly -- otherwise the agent runs with zero messages.
prompt: message.contentParts?.length ? message.contentParts : message.text,
});
});
Agent 是框架无关的:agent 可接受任何 AG-UI AbstractAgent,包括远程的。Python 或 .NET Agent 可以原地不动,由一个小的 TypeScript 宿主代理到它 —— 采用 Channel 从不意味着移植 Agent。
第 3 步:将 Channel 传给运行时
import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
const intelligence = new CopilotKitIntelligence({
apiKey: process.env.CPK_INTELLIGENCE_API_KEY!,
});
const runtime = new CopilotRuntime({
// A Channel supplies its own agent, so runtime-hosted agents are optional here.
agents: {},
intelligence,
identifyUser: (request) => resolveUserFromSession(request),
channels: [support],
});
第 4 步:挂载一个长期运行的宿主
import { createServer } from "node:http";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
const listener = createCopilotNodeListener({
runtime,
basePath: "/api/copilotkit",
});
createServer(listener).listen(Number(process.env.PORT ?? 8300));
// Optional on this mount, and worth doing: it turns a failed activation into a
// startup failure instead of a line in the logs.
await listener.channels.ready({ timeoutMs: 30_000 });
你是否必须调用 ready() 取决于挂载方式
这是关于 Channel 最容易被误解的一点。激活并非在所有地方都是惰性的。
| 挂载 | 激活 | 是否需要 ready()? |
|---|---|---|
createCopilotNodeListener | 监听器创建时启动 | 否 —— await 它以观察结果 |
createCopilotExpressHandler | 路由器创建时启动 | 否 —— await 它以观察结果 |
createCopilotHonoHandler | 推迟到首次 ready() | 是 |
createCopilotRuntimeHandler(通用 fetch) | 推迟到首次 ready() | 是 |
任何带 activateChannels: false 的挂载 | 无;没有控制面,没有 socket | 不会建立任何连接 |
这两个自行管理生命周期的包装器自行启动,因为它们掌控自己的进程生命周期 —— 一个已声明的 Channel 因为被声明而连接,没有什么咒语会被遗忘。fetch 和 hono 处理器则有意推迟:它们是 serverless 和 edge 的入口点,在那里 isolate 会随每次请求冻结和回收,而独立的冷启动会为同一个 Channel 制造出相互竞争的监听器。
因此,那种静默失败 —— 一个提供 HTTP 服务、看起来健康、却什么都不回答的进程 —— 发生在推迟型挂载上,且始终没有任何东西调用 ready() 时。在 node 或 express 宿主上,缺少 ready() 并不是那种失败。
无论哪种情况,都有两个细节:
-
ready()是一次性且幂等的。它会在初始激活结果上落定,因此在自动启动后再 await 它,是观察那次激活,而不是触发第二次激活。它可能以setup_required解决,这意味着激活已落定 —— 而不是投递是健康的。只有status().overall === "online"才应视为已连接。 -
将激活与关闭配对,这样重新部署时能干净地释放连接:
process.on("SIGTERM", async () => { await listener.channels.stop(); process.exit(0); });
部署形态
判断项目是否已经拥有一个长期运行的宿主,正是这项技能存在的意义。阅读项目自身的依赖和启动脚本,弄清它目前运行的是什么,然后:
| 项目目前运行的内容 | 应该怎么做 |
|---|---|
| 独立的 Node/Express/Hono 服务 | 在它已有的运行时上声明 Channel |
| 运行在 Vercel、Lambda 或边缘函数上的 Next.js | 为 Channel 添加一个独立的长期运行进程 |
| Python 或 .NET Agent | 不要动它;添加一个小的 TypeScript channel 宿主来做代理 |
独立进程并非强制要求。如果项目已经运行着长期运行的运行时——例如一个为 React UI 提供服务的运行时——那么同一个运行时就可以声明 Channel。只有当现有运行时是无服务器的时候,才需要第二个进程。
当需要独立宿主时,它就是一个小程序:运行时、Channel,以及被 await 的 ready 调用。它不需要为 UI 提供服务,也完全不需要 HTTP 服务器——没有任何东西会调用它。网关连接是出站的,保持它打开正是让进程存活的原因。脚手架生成的 channel-host.mts 正是如此,即使手写也值得将其作为参考实现来阅读。
绝不要做这些事
- 不要把提供方凭据写进项目,用于托管 Channel。Intelligence 会在服务端存储它们;运行时从不读取它们。如果项目持有一个托管 Channel 的 Slack bot token,说明某处接线错了。
- 不要把 Channel 放在无服务器的路由处理器上。 它看起来会部署成功,但永远不会连接。
- 不要假设
ready()是必需的,也不要假设它是可选的。 检查挂载情况。让人添加一个他们并不需要的调用,和漏掉一个他们确实需要的调用一样没有帮助。 - 不要把
runner与intelligence一起传递。 这正是让线程悄悄留在内存中的原因。 - 不要手工把 Channel 接入脚手架生成的项目。 如果
channel-host.mts存在,代码部分就已经完成了;添加第二个声明会让宿主无法启动,而不是添加一个 bot。 - 不要臆造 Channel 基础设施 id。 项目、适配器和 channel id 都派生自 Intelligence 配置加上 Channel 的
name。 - 不要通过 Intelligence 发送 Teams 图标或包字节。 浏览器生成的产物只保留在当前尝试的本地,之后会被丢弃。
- 不要在正常的托管 Teams 设置中使用 Azure Bot。 启动路径是 Teams Developer Portal 加 Entra,或者 Teams 托管的 Fast CLI 路径。
- 不要因为设置显示 Created and installed 就认为 Teams Channel 能正常工作。 提供方完成与运行时/消息健康是两个独立的维度。