Microsoft 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。
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 导出:
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 资源特定同意权限。
渠道模块
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。
绑定工具
'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。当不允许重复入站时,在分发之前在应用自有的持久存储中声明它们。