@copilotkit/channels-teams

中文文档
0.12.0发布于 2026/10/28MITTypeScriptES Module构建来源已验证近 30 天下载 105.1万

Microsoft Teams platform adapter for CopilotKit JSX channels (@copilotkit/channels).

原始使用文档
文档目录10 节
  1. Managed Channels: the alternative to holding your own crede…
  2. Install
  3. Quickstart
  4. How it maps onto the PlatformAdapter contract
  5. Native Teams JSX
  6. Options
  7. Request authentication
  8. Inbound attachment downloads
  9. Status & roadmap
  10. Exports

@copilotkit/channels-teams

The Microsoft Teams platform adapter for @copilotkit/channels. It's a concrete PlatformAdapter that plugs Teams into the platform-agnostic bot engine, exactly like @copilotkit/channels-slack does for Slack. You write your bot once with createChannel (handlers, JSX, tools, context) and run it on Teams by adding this adapter.

It is built on the Microsoft 365 Agents SDK (@microsoft/agents-hosting), the successor to the Bot Framework SDK.

The adapter keeps its own Teams/Microsoft 365 credentials (clientId / clientSecret / tenantId, or none for anonymous local dev) — in the managed path the Channel runs inside a CopilotKit Intelligence-configured CopilotRuntime (free plan available), which starts and owns the channel's lifecycle. Building and operating your own channel runner on the SDK primitives is also a supported path.

Managed Channels: the alternative to holding your own credentials

This adapter is the self-hosted path: your process holds the Microsoft Teams credentials, runs the Microsoft Teams ingress, and talks to Microsoft Teams directly.

Managed Intelligence Channels is the alternative. Intelligence owns the provider edge — signed ingress, egress, and encrypted credential storage — so your process holds no Microsoft Teams credentials and exposes no public Microsoft Teams endpoint. You also get durable threads, the Channels dashboard with per-Channel health and transcripts, and guided provider setup from either the browser wizard or the CLI. For a newly created managed app, the browser creates the durable Channel draft and issues the fully scoped provisioning command:

npx copilotkit@latest channels add --project-id <project-id> --channel-id <channel-id> --adapter teams --provision

That managed path creates a Teams-managed bot and Entra identity; it does not require Azure Bot. The peer manual path uses Teams Developer Portal plus Entra. Both keep provider secrets and one-time app-package bytes out of your project.

Your bot code is otherwise identical — the agent, tools, context, commands, and turn handlers do not change. Only the transport does. See examples/teams/app/managed.ts for the same bot wired both ways, and the copilotkit-channels skill for the runtime wiring.

This self-hosted adapter remains fully supported. Choose it when you want the provider connection inside your own infrastructure.

Install

pnpm add @copilotkit/channels @copilotkit/channels-ui @copilotkit/channels-teams
pnpm add @microsoft/agents-hosting @microsoft/agents-activity

The Microsoft 365 Agents SDK packages are optional peer dependencies, so they are not installed for you. Add them as shown above — this adapter is the self-hosted path, and it needs them to run the Teams ingress.

express is an optional peer too, but you only need it if you use the built-in listener, createTeamsServer. It is loaded lazily inside start(), so a bot that serves POST /api/messages from its own HTTP server does not install it:

pnpm add express   # only for createTeamsServer

They are optional because the managed path does not need them. Managed Channels reach this package only through the ./render subpath, which never touches the Microsoft SDK, so an app that installs @copilotkit/runtime for a plain chat UI no longer pulls the whole Microsoft Agents stack, or the exact zod pin that comes with it.

If you import the adapter without them, Node fails at import time and names the package to install:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@microsoft/agents-hosting'

Quickstart

import { createChannel } from "@copilotkit/channels";
import { teams } from "@copilotkit/channels-teams";
import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";

const bot = createChannel({
  name: "support-bot", // project-unique Intelligence Channel name
  identifyUser: "platform",
  adapters: [teams({ port: 3978 })],
});

bot.onMessage(({ thread, message }) => thread.post(`Echo: ${message.text}`));

// The runtime owns the channel's lifecycle — there is no `bot.start()`.
const runtime = new CopilotRuntime({
  intelligence: new CopilotKitIntelligence({
    // apiUrl and wsUrl default to cloud-hosted CopilotKit Intelligence — override
    // both together only for a self-hosted deployment.
    apiKey: process.env.CPK_INTELLIGENCE_API_KEY!, // free tier available
  }),
  channels: [bot],
});

// Creating the listener starts the Channel's connection.
const listener = createCopilotNodeListener({ runtime });
// Optional: await that activation; once settled, POST /api/messages is listening
// on :3978.
await listener.channels.ready();

Then point the Microsoft 365 Agents Playground at it. No Microsoft credentials are required for local development:

npx @microsoft/m365agentsplayground   # opens http://localhost:56150

The Playground connects to http://127.0.0.1:3978/api/messages and gives you a Teams-like chat UI to test against. See examples/teams for a complete, runnable echo bot, and the Microsoft Teams guide for sideloading into real Teams via Azure Bot Service.

How it maps onto the PlatformAdapter contract

  • Ingress: a CloudAdapter receives Teams activities at POST /api/messages (stood up by an Express server). Each message activity is normalized into sink.onTurn(...). Uploaded files ride along as attachments: buildFileContentParts downloads them (a file.download.info URL, or a data:/https media URL) and hands the agent multimodal content parts — CSV/JSON/text as decoded text, images and PDFs as binary. That's what makes "upload a CSV → get a chart" work. Note Teams only delivers uploaded files to a bot in 1:1 (personal) chat (requires supportsFiles: true in the app manifest); in a channel or group chat Teams does NOT send the file to the bot at all, so chart-from-data there means pasting the data inline.
  • Egress: structured/interactive UI is rendered to an Adaptive Card (1.5) and sent as an attachment; a reply that collapses to plain text is sent as a normal text activity (a bare Echo: hi shouldn't be a card). Both go out on the live TurnContext within the originating turn. The engine awaits the whole turn handler, so a reply (or a full runAgent() loop) completes before the HTTP response closes. (Out-of-turn / proactive sends fall back to CloudAdapter.continueConversation via the captured conversation reference.)
  • Files out: postFile posts a file to the conversation. An image (e.g. a rendered chart PNG) is sent as an inline attachment via a data: URI, so it renders directly in the thread — the bot-slack postFile parallel.
  • Streaming: text replies stream by message edit (Teams' baseline model). It posts the first content, then updateActivity edits the same message as the buffer grows (throttled and serialised; see TeamsMessageStream), after a typing indicator. Native token streaming is a later enhancement.
  • Agent runs: createRunRenderer bridges AG-UI events to Teams. Each text message is streamed by edit, and tool calls plus interrupts are captured for the run loop.
  • History: Teams does not hand the bot a queryable transcript, so an in-memory TeamsConversationStore keeps one per conversation and seeds each agent run with it. Swap in a durable ConversationStore for production.

Native Teams JSX

Use the Teams namespace for Adaptive Card types outside the portable JSX set. A native card has one explicit Teams.AdaptiveCard root. Actions can be root children or children of Teams.ActionSet.

import { Teams } from "@copilotkit/channels-teams";

await thread.post(
  <Teams.AdaptiveCard fallbackText="Deploy approval">
    <Teams.TextBlock text="Deploy ready" wrap />
    <Teams.ActionSet>
      <Teams.Action.Submit
        key="approve"
        title="Approve"
        value={{ decision: "approve" }}
        onSubmit={({ action }) => approve(action.value)}
      />
    </Teams.ActionSet>
  </Teams.AdaptiveCard>,
);

The serializer computes the card version from the types and properties in use. An explicit lower root version fails with the component and property that raised the minimum. Named child slots stay traversable, so handlers inside an action set survive managed delivery and action recovery. Teams.Raw accepts a reviewed non-interactive Adaptive Card object.

The generated native catalog labels the 38 stable Teams body types, 7 stable actions, preview entries, and supporting nodes. Host badges and catalog presence come from adaptivecards.microsoft.com. “Supported” here means Microsoft marks the entry for Teams; verification in a live tenant remains a separate release check. Direct Teams and managed Teams use the same serializer and Bot Framework attachment shape.

Options

teams({
  port: 3978, // POST /api/messages port (Playground default)
  host, // interface to listen on (see below)
  clientId, // Microsoft app id; omit for anonymous local dev
  clientSecret, // omit for anonymous local dev
  tenantId, // omit for multi-tenant / anonymous
  interruptEventNames, // custom-event names treated as agent interrupts
  allowedServiceUrls, // extra Bot Connector origins to accept (see below)
});

Credentials also resolve from the clientId / clientSecret / tenantId environment variables, or from Connections__<id>__Settings__ClientId / ClientSecret / TenantId (the names the M365 Agents SDK reads). The Bot Framework SDK's MicrosoftAppId / MicrosoftAppPassword variables are not read; if any of them is set and no app id resolves, start fails instead of running unauthenticated. Rename them to one of the supported forms.

Request authentication

With a clientId configured (as an option, or through any of the env vars the M365 Agents SDK reads, including Connections__*), POST /api/messages only accepts requests that carry a valid token for that app. The SDK's authorizeJWT checks the signature, audience and expiry. The listener then requires the bot's own app id as the audience, and an issuer the SDK configured for the app (Bot Framework, https://api.botframework.com, or your tenant's Azure AD issuers; the botframework.com tenant when no tenantId is set). A Bot Framework token must also carry a serviceurl claim, and its signing key must be endorsed for the activity's channelId (see below). Anything else gets 401 or 403 and never reaches your handlers. Settings such as authorityEndpoint, or a custom issuer list (Connections__<id>__Settings__Issuers), resolve from the environment the same way the SDK reads them.

The Connections__* env settings take precedence: when any of them is set, the SDK builds its auth settings from those env vars alone and ignores the clientId / clientSecret / tenantId options. If the app id it resolves is missing or differs from the clientId you configured, start fails with an error that names the conflict, rather than running anonymously or as another app.

The bot sends its app token to the activity's serviceUrl when it replies, so that URL is checked too. It must be one of the Microsoft connector hosts below and, when the token carries a serviceurl claim, have the same origin as that claim. Otherwise the request gets 403, and the bot never sends to it:

  • https://smba.trafficmanager.net (Teams)
  • https://smba.infra.gcc.teams.microsoft.com (Teams GCC)
  • https://smba.infra.gov.teams.microsoft.us (Teams GCC High)
  • https://smba.infra.dod.teams.microsoft.us (Teams DoD)
  • https://*.botframework.com (Bot Framework channels such as Web Chat)
  • https://msteams.botframework.azure.cn (Teams operated by 21Vianet)
  • https://*.botframework.azure.us (Azure Government)

Add any other connector origin with allowedServiceUrls: ["https://connector.example"]. A leading *. on the host allows any subdomain, and needs at least two labels after it (https://*.example.com, not https://*.com). Plain http is accepted only for localhost, 127.0.0.1 and [::1]. An entry that breaks these rules is ignored with a warning.

To test a credentialed bot with the Bot Framework Emulator, allow the Emulator's local connector, for example allowedServiceUrls: ["http://localhost:<port>"].

Channel endorsements. Each key in the Bot Connector's signing-key document lists the channels it may sign for. For a Bot Framework token (iss: https://api.botframework.com) whose key lists endorsements, the activity's channelId must be one of them, or the request gets 403. Like the Bot Framework SDK, keys whose endorsements field is absent or empty are not checked; Azure AD keys (single-tenant Azure Bot Service, the Bot Framework Emulator) have none. The key document is cached for an hour; fetching it times out after 5 seconds, and a failed fetch rejects the request (503).

Anonymous mode. Without a clientId the endpoint is anonymous: anyone who can reach it can drive a turn. That mode is for the local M365 Agents Playground only, so the listener binds to 127.0.0.1 unless you set host. That is IPv4 only: a client that connects to localhost over IPv6 (::1) without falling back to IPv4 needs host: "localhost" or host: "::1". Setting a non-loopback host (for example host: "0.0.0.0") exposes an unauthenticated bot to the network; the adapter logs a warning. With a clientId the listener binds all interfaces unless host says otherwise.

createTeamsServer. Called directly, it uses the auth settings of the CloudAdapter you pass. An authConfig override must resolve to that adapter's app id: one that resolves to no app id (for example {}) or to another app makes start() fail, so an override can never turn a credentialed adapter anonymous or switch its identity.

If you serve POST /api/messages yourself instead of using createTeamsServer, do the same checks before CloudAdapter.process: run authorizeJWT, accept only your app id as the audience and only its issuers, check endorsements for Bot Framework tokens, and check that the activity's serviceUrl is a connector you trust and matches the token's serviceurl claim. authorizeJWT alone accepts any issuer and does not look at serviceUrl, and the bot will send its app token there.

Inbound attachment downloads

Remote attachments require HTTPS on port 443 and a recognized Microsoft file host. The built-in policy accepts tenant subdomains of SharePoint (sharepoint.com, sharepoint.us, sharepoint.cn, sharepoint.de), Teams (teams.microsoft.com, teams.microsoft.us), asm.skype.com, botframework.com, botframework.azure.us, files.1drv.com, and svc.ms, plus graph.microsoft.com and the exact Teams connector hosts listed above. URLs containing credentials or pointing elsewhere are skipped. For local Playground uploads, use bounded data: attachments.

Every download and redirect resolves DNS and rejects the destination if any address is private, loopback, link-local, reserved, or an unsupported IPv6 transition address. The TLS connection uses only the validated addresses and still verifies the original hostname. Redirects are limited to five. Graph file-content redirects use their preauthenticated URL; the Graph bearer token is never forwarded to a redirect destination. Connector allowedServiceUrls does not extend this attachment policy.

The downloader enforces size limits while reading and cancels oversized or timed-out requests. Inline data is bounded before decoding. Configure limits with the adapter's files option:

OptionDefaultScope
maxBytesPerFile8 MiBEach attachment
maxFiles5Download attempts per message, including failed attempts
maxTextBytes200 KiBDecoded text passed to the agent
maxBytesPerMessage40 MiBCombined download bytes, including Graph metadata
downloadTimeoutMs30,000DNS, connection, redirects, and body of each download; each Graph metadata/token operation
maxDownloadTimeMs60,000Shared elapsed download time for the message

The adapter validates download-budget limits when constructed. They must be nonnegative safe integers; invalid values such as Infinity fail immediately with an error naming the option. Each message gets a fresh budget.

The inline and Graph fallback paths share one budget. Failed downloads produce short skip notes without including preauthenticated URLs or transport error details. The Graph channel fallback uses the commercial Graph endpoint.

Status & roadmap

Implemented: message ingress; Adaptive Card rendering of the bot-ui vocabulary (<Header>, <Section>/<Markdown>, <Fields>, <Table>, <Image>, <Actions>/<Button>, <Select>, <Input>, <Context>) with a plain-text path for bare replies and a Markdown table fallback; streamed-by- edit text replies with a typing indicator; runAgent tool-call / interrupt capture; card-action round-trip + HITL (below); conversation history; update / delete. Verified in the M365 Agents Playground.

Card-action round-trip + HITL. Adaptive Card Action.Submit clicks arrive as Message activities carrying the action data in activity.value; decodeInteraction parses our opaque ckActionId + button value and routes them to sink.onInteraction, which resolves the engine's awaitChoice waiter and runs the button's onClick (e.g. to edit the picker in place). A tool handler that calls await thread.awaitChoice(<Card/>) therefore gates the agent on a human decision; see examples/teams for an approve/reject demo. Ingress and interaction decoding derive the conversation key from one shared helper (conversationKeyOf) so the waiter always resolves.

Async turn handoff. When credentialed, ingress acks the inbound turn immediately and runs the agent on a detached continueConversation context, so an awaitChoice suspend can outlive the Teams turn window (approval minutes later). In the anonymous local Playground (where continueConversation has no app id) the run uses the inbound turn context, which localhost holds open across the suspend. Waiters are in-memory (v1), so they don't survive a process restart.

Planned follow-ups (the architecture leaves room for each):

  • Native token streaming: token-by-token replies via the SDK's StreamingResponse (queueInformativeUpdate / queueTextChunk / endStream), vs. the current post-then-edit model.
  • Durable HITL waiters: persist pending awaitChoice state so approvals survive a restart (today they're in-memory).
  • User lookup (Microsoft Graph) and arbitrary non-image file upload via the Teams/Graph file-consent flow (today postFile handles inline images).

Exports

teams, TeamsAdapter, TeamsAdapterOptions, TeamsReplyTarget, ConversationKey; TeamsConversationStore; createRunRenderer; conversationKeyOf / parseCardAction; renderTeamsMarkdown; renderAdaptiveCard / AdaptiveCard / isPlainText / ADAPTIVE_CARD_CONTENT_TYPE; TEAMS_LIMITS; TeamsMessageStream; createTeamsServer / TeamsServer / TeamsServerConfig; SanitizingHttpAgent (deprecated — Channels sanitize by default); buildFileContentParts / TeamsAttachmentRef / FileDeliveryConfig.

版本与发布记录

@copilotkit/channels-teams 共发布 46 个版本,首次发布于 2026/07/09;过去 12 个月发布了 46 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.12.0latest2026/10/2813 天前
0.11.12026/10/1625 天前
0.11.02026/09/231 个月前
0.10.02026/09/151 个月前
0.9.32026/09/151 个月前
0.9.22026/09/032 个月前

其他发布通道:canary → 0.7.3-canary.1785878925。

关于 @copilotkit/channels-teams 的常见问题

@copilotkit/channels-teams 是什么?

Microsoft Teams platform adapter for CopilotKit JSX channels (@copilotkit/channels).

如何安装 @copilotkit/channels-teams?

在项目目录运行 npm install @copilotkit/channels-teams,或使用 pnpm add / yarn add / bun add。

@copilotkit/channels-teams 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@copilotkit/channels-teams 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@copilotkit/channels-teams 可以商用吗?

@copilotkit/channels-teams 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@copilotkit/channels-teams 的下载量有多少?

根据 npm 官方下载统计,@copilotkit/channels-teams 在 2026/10/11 – 2026/11/09 这 30 天内被下载 1,050,936 次,日均约 35,031 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@copilotkit/channels-teams 还在维护吗?

最新版本 0.12.0 发布于 2026/10/28(13 天前);至今共发布 46 个版本,其中过去 12 个月发布了 46 个;首次发布于 2026/07/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。