跳转到内容

输入关键词开始搜索文档。

Microsoft Teams

最后更新 查看 Markdown 源文件@flue/teams

快速开始

使用 Microsoft Teams 蓝图为已有的 Flue 项目添加已认证的 Microsoft Teams Bot Connector 活动和项目自有的出站消息。在终端或你选择的编码智能体中运行以下命令:

flue add channel teams

概述

该蓝图安装 @flue/teams,在源码根目录创建 lib/teams-client.ts Fetch 客户端和 channels/teams.ts 渠道模块,并修改所选智能体以绑定生成的消息工具。Fetch 客户端处理 OAuth 令牌交换和 Bot Connector 请求,无需添加 Microsoft 的 Node 导向的托管 SDK。

src/channels/teams.ts(简略版)
import { dispatch } from '@flue/runtime';
import { createTeamsChannel } from '@flue/teams';
import { Assistant } from '../agents/assistant.ts';

export const channel = createTeamsChannel({
  appId: process.env.TEAMS_APP_ID!,
  tenantId: process.env.TEAMS_TENANT_ID!,
  async activities({ activity }) {
    if (activity.type !== 'message' || !activity.text) return;
    await dispatch(Assistant, {
      id: channel.instanceId(channel.destination(activity)),
      message: {
        kind: 'signal',
        type: 'teams.message',
        body: activity.text,
        attributes: {
          ...(activity.id === undefined ? {} : { activityId: activity.id }),
          senderId: activity.from.id,
          senderName: activity.from.name,
        },
      },
    });
  },
});

简略版示例省略了生成的客户端和消息工具。配置完成后,文本活动会继续处理其已验证 Teams 对话的智能体实例,绑定的工具可以向相同的 Connector 服务 URL 和线程发送回复。生成的 Fetch 客户端在 Node 和 Cloudflare Workers 上均可运行。

挂载渠道

渠道仅在 app.ts 挂载它的地方提供 HTTP 路由。挂载模块的命名 channel 导出:

src/app.ts
import { channel as teams } from './channels/teams.ts';

app.route('/channels/teams', teams.route());

channel.route() 是一个纯路由器工厂,相对于挂载路径提供渠道声明的路由。本指南中的 webhook 路径假定使用常规的 /channels/teams 挂载路径;不同的挂载路径会相应调整。分发目标智能体模块携带 'use agent' 指令——该指令会注册它,因此仅用于分发的智能体不需要自己的 HTTP 挂载。

配置

变量用途
TEAMS_APP_ID必需 — 限制入站 JWT 受众。
TEAMS_TENANT_ID必需 — 限制活动租户标识。
TEAMS_APP_PASSWORD必需 — 验证出站 OAuth 请求。

它为已认证的 Bot Connector 入站安装 @flue/teams,并为出站消息创建项目自有的 Fetch 客户端。

Microsoft 当前的 JavaScript Agents 和 Teams SDK 声明了 Node 运行时,并使用 Node 导向的认证或托管包。蓝图通过 Fetch 直接使用相同的文档化 OAuth 客户端凭据和 Bot Connector REST 协议,因此集成在 Node 和 Cloudflare Workers 上均可运行。

将 Azure Bot 消息端点设置为:

https://example.com/channels/teams/activities

Teams 机器人默认在被提及时接收通道消息。当应用需要接收所有通道或群聊消息时,配置相应的 Teams 资源特定同意权限。

渠道模块

src/channels/teams.ts
import { defineTool, dispatch } from '@flue/runtime';
import { createTeamsChannel } from '@flue/teams';
import * as v from 'valibot';
import { Assistant } from '../agents/assistant.ts';
import { createTeamsClient, type TeamsMessageRef } from '../lib/teams-client.ts';

const appId = process.env.TEAMS_APP_ID!;
const tenantId = process.env.TEAMS_TENANT_ID!;

export const client = createTeamsClient({
  appId,
  tenantId,
  appPassword: process.env.TEAMS_APP_PASSWORD!,
});

export const channel = createTeamsChannel({
  appId,
  tenantId,

  // 路径:/channels/teams/activities
  async activities({ activity }) {
    switch (activity.type) {
      case 'message': {
        if (!activity.text) return;
        const destination = channel.destination(activity);
        await dispatch(Assistant, {
          id: channel.instanceId(destination),
          // 此事件创建实例时记录一次;之后忽略。
          initialData: {
            serviceUrl: destination.serviceUrl,
            conversationId: destination.conversationId,
            botId: destination.botId,
            ...(destination.threadId === undefined ? {} : { threadId: destination.threadId }),
          },
          message: {
            kind: 'signal',
            type: 'teams.message',
            body: activity.text,
            attributes: {
              ...(activity.id === undefined ? {} : { activityId: activity.id }),
              senderId: activity.from.id,
              senderName: activity.from.name,
            },
          },
        });
        return;
      }
      default:
        return;
    }
  },
});

export function postMessage(ref: TeamsMessageRef) {
  return defineTool({
    name: 'post_teams_message',
    description: '发送到绑定到此智能体的 Microsoft Teams 对话。',
    input: v.object({ text: v.pipe(v.string(), v.minLength(1)) }),
    async run({ data: { text } }) {
      const result = await client.postMessage(ref, text);
      return { output: { activityId: result.id } };
    },
  });
}

生成的 lib/teams-client.ts 将应用凭据交换为 Bot Connector 令牌,在过期前缓存它,并通过已验证目标的 Connector 服务 URL 发送消息活动。

回调接收提供商原生的 Bot Framework Activity,从 botframework-schema 重新导出。对原生 activity.type(message、conversationUpdate、invoke、messageReaction 和其他 Bot Framework 类型)进行分支处理,并读取 Microsoft 文档化的字段名称。当需要寻址回复时,调用 channel.destination(activity) 派生规范的路由标识。返回空内容产生空的 200 响应,返回 JSON 用于提供商体,或使用 Hono 上下文进行显式状态控制。

Azure Bot Service 用实际响应窗口保持入站请求打开,因此及时提交持久性工作——dispatch(...) 活动并返回,然后依靠幂等性而不是在慢操作上阻塞响应。invoke 活动期望 JSON 确认体,Bot Connector 在任何非 2xx 响应上重试,因此在工作安全入站后返回 2xx。

绑定工具

src/agents/assistant.ts
'use agent';
import { useInitialData, useModel, useTool } from '@flue/runtime';
import * as v from 'valibot';
import { postMessage } from '../channels/teams.ts';

const initialData = v.object({
  serviceUrl: v.string(),
  conversationId: v.string(),
  botId: v.string(),
  threadId: v.optional(v.string()),
});

export function Assistant() {
  useModel('anthropic/claude-haiku-4-5');
  const data = useInitialData<v.InferOutput<typeof initialData>>();
  if (!data) throw new Error('This agent is created by the Microsoft Teams channel dispatch.');
  useTool(postMessage(data));
  return 'Reply concisely in the bound Microsoft Teams conversation.';
}

Assistant.initialData = initialData;

模型仅选择消息文本。受信任的代码将 Connector 服务 URL、对话、机器人账户和通道线程绑定为实例的创建数据——智能体使用 useInitialData() 读取它们,而不是解析实例 ID。

认证

@flue/teams 在调用处理程序之前验证 Bot Connector 承载令牌。它检查:

  • Microsoft OpenID 签名密钥和 RS256 签名;
  • 颁发者、应用受众和过期时间;
  • 签名密钥的 msteams 背书;
  • 活动的精确 serviceUrl 与签名令牌声明的对比;
  • 主对话和通道租户与 TEAMS_TENANT_ID 的对比。

默认目标是 Microsoft 公有云。支持的主权部署可以提供其文档化的 OpenID 元数据 URL、令牌颁发者和 OAuth 授权机构。

该包不去重活动 ID。当不允许重复入站时,在分发之前在应用自有的持久存储中声明它们。

参见 @flue/teams README。