ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用 Flue 构建 Microsoft Teams 频道:基于 Bot Connector REST 协议的沙箱 Agent 接入实战

用 Flue 构建 Microsoft Teams 频道:基于 Bot Connector REST 协议的沙箱 Agent 接入实战 用 Flue 构建 Microsoft Teams 频道基于 Bot Connector REST 协议的沙箱 Agent 接入实战【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue本文以仓库中的 teams-channel 示例 为骨架讲解如何在不依赖微软官方 Node SDK 的前提下用纯 Fetch 实现一个可同时运行在 Node 与 Cloudflare Workers 上的 Microsoft Teams 机器人频道包括 Bot Connector 活动入站校验、OAuth client-credentials 出站发消息、规范化会话身份与 Agent 实例路由。读完本文你将掌握在 Fluesandbox agent framework中集成 Teams 频道的完整配置、路由、鉴权与部署方案。示例概览一个无 SDK的 Teams 频道Flue 仓库中的 teams-channel 示例 演示了以下关键技术点在/channels/teams/activities接收经过鉴权的 Bot Connector activities活动使用固定的应用身份与租户身份fixed application and tenant identity采用显式 dispatch 路由explicit dispatch routing即收到活动后显式调用dispatch创建 Agent 实例基于规范化会话身份canonical conversation identity生成稳定的实例 ID使示例保持无状态stateless使用项目自持的一个 Fetch 客户端完成所有出站消息发送。为什么不用微软官方 SDKREADME 明确指出微软当前的 JavaScript Agents 与 Teams SDK 依赖 Node并使用面向 Node 的认证与托管包。本示例改为直接实现有文档可依的OAuth client-credentials 与 Bot Connector REST 协议通过 Fetch从而让同一份项目代码既能跑在 Node 上也能跑在 Cloudflare Workers 上。环境变量与配置参数当构建出的应用启动时以下三个环境变量是必需的TEAMS_APP_ID、TEAMS_TENANT_ID、TEAMS_APP_PASSWORD而构建与类型检查并不需要真实凭据。此外还有三个可选变量用于在受支持的主权云sovereign cloud中覆盖公有云端点。环境变量必填用途源码位置TEAMS_APP_ID是Microsoft Entra 应用 ID用于入站 JWT 的 audience 校验与出站 OAuth 的 client_idexamples/teams-channel/src/channels/teams.tsTEAMS_TENANT_ID是期望的 Microsoft Teams 租户 ID入站活动中的所有 tenantId 必须与之完全一致packages/teams/src/routes.tsTEAMS_APP_PASSWORD是应用密码作为 OAuth client_secretexamples/teams-channel/src/channels/teams.tsTEAMS_OAUTH_AUTHORITY否OAuth 令牌端点前缀默认https://login.microsoftonline.com/${tenantId}examples/teams-channel/src/lib/teams-client.tsTEAMS_OPENID_METADATA_URL否Bot Framework OpenID 元数据 URL默认https://login.botframework.com/v1/.well-known/openidconfigurationpackages/teams/src/auth.tsTEAMS_TOKEN_ISSUER否期望的 Bot Connector token 签发者默认https://api.botframework.compackages/teams/src/auth.ts在 channels/teams.ts 中可以看到环境变量的读取方式requiredEnv在缺失时直接抛错${name} is required.optionalEnv则返回undefined并通过展开运算符只在有值时把覆盖项传入配置。应用骨架挂载 Agent 路由与频道路由应用使用 Hono 作为 Web 框架入口文件 app.ts 只有十几行import { createAgentRouter } from flue/runtime/routing; import { Hono } from hono; import { Assistant } from ./agents/assistant.ts; import { channel } from ./channels/teams.ts; const app new Hono(); app.route(/agents/assistant, createAgentRouter(Assistant)); app.route(/channels/teams, channel.route()); export default app;/agents/assistant由 Flue runtime 提供的 Agent HTTP 路由createAgentRouter/channels/teamsTeams 频道子应用其下实际挂载POST /activities组合出完整的POST /channels/teams/activities端点。在 packages/teams/src/index.ts 中可以看到频道路由的定义方式createTeamsChannel内部生成{ method: POST, path: /activities, handler }再由createChannelRouter包装成可挂载的 Hono 子应用。构建配置由 vite.config.ts 通过flue/vite的flue()插件完成。入站链路活动如何被鉴权与放行createTeamsChannel来自flue/teams返回一个频道对象其activities回调是应用侧唯一需要实现的逻辑。以 channels/teams.ts 为例async activities({ activity }) { if (activity.type ! message || !activity.text) return; const destination channel.destination(activity); await dispatch(Assistant, { id: channel.instanceId(destination), initialData: { /* ... */ }, message: { kind: signal, type: teams.message, body: activity.text, attributes: { /* activityId / senderId / senderName */ }, }, }); },回调收到的activity已经是验证过但未被改写的 provider-native Bot Framework 活动见 packages/teams/src/index.ts 的TeamsActivitiesHandlerInput注释。真正执行鉴权的是底层 handler packages/teams/src/routes.ts其完整检查顺序为Content-Type 校验必须是application/json否则返回415Content-Length 校验非法数字返回400超过bodyLimit默认 1 MiB见 routes.ts返回413Bearer Token 校验解析Authorization头并验签签名密钥发现失败如上游不可用返回503其余验签失败返回401请求体读取与 JSON 解析读取失败或超限返回400/413解析失败返回400频道声明校验channelId必须为msteams否则403token 的endorsements必须包含msteams否则401活动serviceUrl必须与 token 内的serviceurl声明一致否则401租户校验收集活动conversation.tenantId与channelData.tenant.id中的全部 tenant id任一不等于配置的TEAMS_TENANT_ID即返回403目的地可推导校验调用deriveDestination确认活动具备回复所需的最小结构否则400。JWT 验签的底层实现验签逻辑位于 packages/teams/src/auth.ts使用jose库完成仅接受RS256算法且必须携带kid通过 OpenID 元数据端点发现签名密钥并对签发者issuer与jwks_uri做 HTTPS 校验验签时要求audience appId、issuer tokenIssuer、exp与serviceurl声明必须存在时钟容忍 5 分钟密钥集带缓存默认 TTL 1 小时下限 60 秒、上限 24 小时避免max-age0导致每个活动都触发两次上游发现请求未知kid触发强制刷新且冷却 30 秒发现失败同样进入冷却防止打爆上游。这套设计与仓库中 google-chat 等频道的密钥缓存策略一致源码注释中亦有说明。规范化会话身份与实例 ID频道不依赖外部存储来区分会话channel.destination(activity)从活动推导出TeamsConversationRef见 packages/teams/src/routes.ts包含tenantId、serviceUrl、conversationId、scope、botId、可选的threadId/teamId/channelId。scope归一化为四种取值personal一对一私聊groupChat群聊channel频道内此时threadId取replyToId ?? activityIdunknownchannel.instanceId(ref)则把上述身份编码成一个稳定、带命名空间的字符串见 packages/teams/src/index.tsteams:v1:tenantId:scope:serviceUrl:conversationId:botId:threadId:teamId:channelId其中serviceUrl是被验签过的来自 JWT 的serviceurl声明而非直接信任请求体因此该 ID 可用于无状态路由。但要注意 README 的告诫instance id 只校验语法不构成授权能力。本示例中的 Agent 刻意只做 dispatch-only任何直接路由在使用调用方提供的 instance id 做出站请求前都必须独立完成授权。出站链路OAuth client-credentials 与发消息出站消息由示例自持的 Fetch 客户端完成实现在 lib/teams-client.ts获取访问令牌teams-client.tsbody: new URLSearchParams({ grant_type: client_credentials, client_id: options.appId, client_secret: options.appPassword, scope: https://api.botframework.com/.default, }),令牌端点 oauthAuthority /oauth2/v2.0/token默认即https://login.microsoftonline.com/tenantId/oauth2/v2.0/token令牌在内存中缓存expiresAt距当前时间不足 60 秒即视为过期并刷新构造端点前会校验 authority 必须是合法 HTTPS URL无用户名/密码/查询串/片段。发送消息活动teams-client.tsconst response await fetcher(activityUrl(ref), { method: POST, headers: { authorization: Bearer ${token}, content-type: application/json, }, body: JSON.stringify({ type: message, from: { id: ref.botId }, conversation: { id: ref.conversationId }, ...(ref.threadId undefined ? {} : { replyToId: ref.threadId }), text, }), });目标 URL 为serviceUrl v3/conversations/conversationId/activities若存在threadId则追加/threadId实现线程内回复响应必须携带字符串id否则视为无效响应。同时该 URL 构造同样强制 HTTPS 与干净的 URL 形态。Agent 侧initialData 与出站工具Agent 定义在 agents/assistant.ts使用use agent指令const initialDataSchema v.object({ serviceUrl: v.string(), conversationId: v.string(), botId: v.string(), threadId: v.optional(v.string()), conversationName: v.optional(v.string()), }); export function Assistant() { useModel(anthropic/claude-haiku-4-5); const data useInitialDatav.InferOutputtypeof initialDataSchema(); if (!data) throw new Error(This agent is created by the Microsoft Teams channel dispatch.); useTool(postMessage(data)); const conversationName data.conversationName ? ${data.conversationName} : ; return Reply concisely in the bound Microsoft Teams conversation${conversationName}.; } Assistant.initialData initialDataSchema;关键点initialData在频道 dispatch 创建实例时记录一次之后被忽略见 channels/teams.ts 注释Agent 通过useInitialData拿到绑定的会话地址post_teams_message工具channels/teams.ts由defineTool定义输入为最小长度 1 的text运行后调用客户端发消息并返回新活动的activityId入站消息以kind: signal的信号消息进入 Agentattributes中携带activityId、senderId、senderName便于追溯来源。关于循环导入的说明README 特别指出频道模块导入 AgentAgent 又导入频道postMessage这一循环是安全的——因为导入的绑定只在活动回调与 Agent 初始化器内部模块求值完成之后被读取。这是 Flue 示例中模块组织的一个有意设计。幂等性与重复投递README 明确警告该包不对 activity id 去重。Bot Connector 在重试时会重复投递活动如果你的场景无法接受重复分发必须在应用自有的持久化存储durable storage中认领claim活动 id 后再进入 dispatch。换言之去重属于应用层职责而非频道层职责。真实交付的部署前提要让机器人真正在 Teams 中收发消息需要满足两个外部前提见 README一个公网 HTTPS 端点用于接收 Teams 投递的活动回调一个配置好的 Azure Bot 消息终结点messaging endpoint指向上述公网端点。结合本仓库的运行方式开发时可通过 Flue 的 Vite 插件本地起服务部署时由于代码只依赖 Fetch 与 Web 标准 API可直接发布到 Cloudflare Workers示例依赖中包含cloudflare/vitest-pool-workersflue/vite亦负责 Cloudflare 入口的打包。构建与类型检查命令见 package.jsonpnpm build # vite build pnpm check:types # tsc --noEmit这两条命令都不需要真实的 Teams 凭据。小结环节要点关键源码入站鉴权完整校验链媒体类型 → 体量 → JWT 验签 → 频道声明 → 租户 → 目的地packages/teams/src/routes.ts验签与密钥发现OpenID 元数据 RS256 带冷却的密钥缓存packages/teams/src/auth.ts会话身份destination/instanceId/parseInstanceId无状态且含验签后的 serviceUrlpackages/teams/src/index.ts出站消息client-credentials 令牌 Bot Connector RESTFetch 实现跨 Node/Workersexamples/teams-channel/src/lib/teams-client.tsAgent 绑定initialData 一次性注入 post_teams_message工具examples/teams-channel/src/agents/assistant.ts一句话总结Flue 的 Teams 频道示例用纯 Fetch 走完 Bot Connector 的入站验签与出站消息全流程既绕开了微软 Node SDK 的运行时绑定又通过规范化身份与显式 dispatch 保持了无状态、可审计、可授权边界清晰的架构。若需进一步参考可对比仓库中 google-chat-channel 示例 与 slack-channel 示例 了解频道层的通用设计模式。【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表