CopilotKit Teams 通道 HITL 按钮动作信封:Adaptive Card `Action.Submit` 的往返协议深度解析

📅 发布时间:2026/9/10 5:18:55
CopilotKit Teams 通道 HITL 按钮动作信封:Adaptive Card `Action.Submit` 的往返协议深度解析
CopilotKit Teams 通道 HITL 按钮动作信封Adaptive CardAction.Submit的往返协议深度解析【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit本文以 CopilotKit 仓库中 button-action-envelope.md 为权威规格深入解析copilotkit/channels-teams中人机协作HITL按钮动作信封HITL button action envelope的完整生命周期渲染器出站发出的 Adaptive CardAction.Submit线上形态、Teams 点击回传的 Message activity、以及解码端必须遵守的判定规则。读完本文你将掌握如何在 CopilotKit 的 Teams 适配器中构建可靠的审批/确认类 HITL 流程awaitChoice等待用户点击并能正确编写或校验任何带外解码out-of-band decode按钮点击的消费者代码。什么是 HITL 按钮动作信封在 CopilotKit Channels 体系中copilotkit/channels-teams是把 Microsoft Teams 接入平台无关的渠道引擎channels-core的适配器与copilotkit/channels-slack对 Slack 的角色完全对称。其中人机协作Human-in-the-loop场景的核心是Agent 向用户渲染一个带按钮的卡片并暂停等待引擎的thread.awaitChoice用户点击按钮后点击事件需要被精确解码回引擎才能恢复被挂起的运行。这个往返过程依赖一个权威的线上数据形态wire shape即本文标题所说的按钮动作信封它是copilotkit/channels-teams渲染出的所有按钮的统一出站形态它定义了 Teams 点击回传时消费者consumer必须按何种规则解码任何带外解码者例如 CopilotKit Intelligence 的 managed-Teams ingress——它深度引入本包的渲染器但运行自己的入站解码逻辑都必须逐字节一致地匹配这个形态否则awaitChoice等待器将永远无法恢复。该规格在仓库中由三个文件共同锚定合同测试src/button-action-envelope.contract.test.ts——从双向锁定信封形态出站发射器Emittersrc/render/adaptive-card.ts中的renderButton入站解码器Decodersrc/interaction.ts中的parseCardAction。出站方向渲染器发出什么当一个Button带有onClick处理器即不是链接按钮时它被渲染为Adaptive Card 顶层的Action.Submit。这里有一个刻意为之的设计决策必须是Action.Submit而不是Action.Execute——Action.Execute需要verb而我们的按钮不使用 verb 机制。不透明的动作 id 和可选的 value 全部搭乘在 action 的data字段上{ type: Action.Submit, title: Approve, data: { ckActionId: ck:approve, // 不透明 id仅当 Button 带 onClick 处理器时才出现 value: { decision: yes }, // 仅当 Button 带 value prop 时才出现 }, style: positive, // 可选positiveprimary 主按钮| destructivedanger 危险按钮 }三条关键规则链接按钮走Action.OpenUrl带urlprop 的Button渲染为Action.OpenUrl不携带任何data——它不是交互式提交永远不会往返round-trip。data的省略规则如果按钮既没有onClickid 也没有valuedata整个被省略。顶层动作Action.Submit出现在卡片的actions顶层数组中而非 body 元素中。源码佐证renderButton的实现细节在 adaptive-card.ts 中renderButton的实现与上述信封一一对应function renderButton(node: ChannelNode): CardAction { const props node.props ?? {}; // Link button → Action.OpenUrl打开 URL不携带提交数据 if (typeof props.url string props.url.length 0) { return { type: Action.OpenUrl, title: truncateText(collectText(node), TEAMS_LIMITS.buttonText), url: props.url, }; } const action: CardAction { type: Action.Submit, title: truncateText(collectText(node), TEAMS_LIMITS.buttonText), }; const id idFromHandler(props.onClick); const data: Recordstring, unknown {}; if (id) data.ckActionId id; if (props.value ! undefined) data.value props.value; if (Object.keys(data).length 0) action.data data; // style 映射primary → positivedanger/destructive → destructive ... }几个值得注意的实现事实id 的来源onClick处理器在进入渲染器之前已经由 action registry 预绑定pre-bound为{ id }形态——即事件 prop 上被盖上了不透明的注册 id 戳。idFromHandleradaptive-card.ts正是从该对象中提取id字符串。这就是ckActionId如ck:approve的产生位置。style 映射JSX 层的primary被映射为 Adaptive Card 的positivedanger/destructive被映射为destructive对应 Teams 客户端的绿色主按钮与红色危险按钮。文本截断按钮标题经过truncateText处理上限为TEAMS_LIMITS.buttonText256 字符保证不超过 Teams 的载荷上限。渲染器是全量的total renderer未知 intrinsic 节点会被跳过actions数组会经clampArray钳制到TEAMS_LIMITS.actions6 个顶层动作——Teams 大约显示 6 个后就会溢出body 元素钳制到TEAMS_LIMITS.bodyElements100 个。这些上限定义在 budget.ts源于 Teams 对单张 Adaptive Card 附件约 28 KB JSON 的硬性限制。入站方向Teams 点击交付什么点击一个Action.Submit按钮Teams 回传的是一个Message activityactivity.type message而不是invoke/adaptiveCard/action/Action.Executeactivity。动作的data会整体变成activity.value且消息的text为空——载荷在value里不在文本里{ type: message, text: , // 空——载荷在 value而非 text value: { // 出站时发出的 action data并与卡片输入合并 ckActionId: ck:approve, value: { decision: yes }, }, conversation: { id: stable conversation id }, }解码规则Decode rules规格对解码者提出四条硬性规则判定是否是卡片动作typeof activity.value.ckActionId string。若不满足这就是一条普通聊天消息应按普通消息处理。字段提取id activity.value.ckActionIdvalue activity.value.value。只搬运这两个字段——禁止任何resume-data 走私no resume-data smuggling持久性durability依赖消费者以id为键的动作存储action store。卡片输入合并如果卡片上还带有Input/Select字段Teams 会把它们的值合并进activity.value与ckActionId/value平级。需要时直接从activity.value中按命名键读取输入值。会话键conversation key从activity.conversation.id推导见conversationKeyOf。ingress 与 interaction 解码必须使用同一个键否则awaitChoice等待器会永久悬空stranded。源码佐证parseCardAction与conversationKeyOfinteraction.ts 中parseCardAction的实现忠实执行了上述规则export function parseCardAction(activity: TeamsActivityLike): | { id: string; value: unknown; values?: Recordstring, unknown } | undefined { const activityValue activity.value as ... | undefined; const data activityValue?.action?.data ?? activityValue; if (!data || typeof data ! object || typeof data.ckActionId ! string) { return undefined; // 不是卡片动作 → 普通聊天消息 } const values Object.fromEntries( Object.entries(activityValue ?? {}).filter( ([name]) name ! action name ! ckActionId name ! value, ), ); return { id: data.ckActionId, value: data.value, ...(Object.keys(values).length 0 ? { values } : {}), }; }补充实现事实兼容action.data嵌套data activityValue?.action?.data ?? activityValue意味着解码器同时兼容直接平铺与嵌套在action下两种回传形态这是对 Teams 不同客户端行为差异的防御性处理。输入值收敛除action/ckActionId/value之外的其余键即卡片Input/Select合并进来的值被收敛为values对象返回对应规则 3 的按命名键读取输入值。conversationKeyOfinteraction.ts极简且稳定直接返回activity.conversation?.id ?? 。测试 interaction.test.ts 专门验证ingress 消息与其后的卡片动作提交必须推导出同一个键并断言无会话 id 时返回空串、绝不抛异常。源码佐证点击在适配器内的路由在 adapter.ts 的handleActivity中入站 message activity 会先经过parseCardAction判定命中则调用sink.onInteraction(...)携带action.id、action.value、action.values与会话键让引擎解析匹配的awaitChoice等待器并执行按钮的onClick例如原地编辑选择器卡片。未命中则走普通聊天消息路径去atbot/at提及、下载附件、sink.onTurn(...)。在有凭据真实 Teams时入站 HTTP turn 立即 ack交互在continueConversation的分离式detachedproactive context 上运行——因为入站点击 turn 的连接器客户端是匿名身份原地updateActivity编辑卡片会被 Connector 以 401 拒绝在匿名本地 PlaygroundM365 Agents Playground无 app id则直接在入站 turn context 上执行。decodeInteractionadapter.ts是同一解码逻辑在PlatformAdapter边界上的公开形态供外部复用同一套信封。合同测试双向锁定的实证button-action-envelope.contract.test.ts是这份规格的可执行注释从两个方向把信封钉死出站断言渲染带onClick: { id: ck:approve }与value: { decision: yes }的按钮断言action.type Action.Submit、没有verb属性、且action.data恰好等于{ ckActionId: ck:approve, value: { decision: yes } }。往返断言把出站的action.data原样当作 Teams 回传的activity.valuetext为空断言parseCardAction解码出{ id: ck:approve, value: { decision: yes } }且conversationKeyOf返回conv-1。负向断言普通聊天消息value: undefined解码结果为undefined——即不是卡片动作。为什么这些约束如此重要Action.Submit而非Action.Execute避免verb语义与 Teams 对adaptiveCard/actioninvoke 的特殊处理让点击以最朴素的 Message activity 回传任何基于 message 的 ingress 都能捕获。无 resume-data 走私信封刻意只携带不透明 id 与极小的 value。恢复运行所需的全部状态存放在消费者的动作存储中以id为键而不是塞进线上载荷。这让信封保持最小、可校验、可审计也让 managed ingress 与自托管 ingress 能够共享同一形态。会话键单一来源conversationKeyOf是 ingressonTurn与交互解码必须共用的推导函数。任何一侧私自改用不同推导方式都会让awaitChoice等待器静默悬空——这是规格中反复强调的失败模式。HITL 的时间尺度适配器对awaitChoice的支持依赖分离式 turn 交接——在真实 Teams 中点击可能发生在几分钟后adapter.ts 中ackDeadlineMs 15000声明了入站 turn 的实际窗口。注意当前等待器是内存态的v1进程重启后不会存活这是 README 明确标注的后续计划项。落地实践如何消费这份信封无论你走自托管适配器路径进程持有 Microsoft Teams 凭据直接跑 ingress还是走managed Intelligence Channels路径Intelligence 拥有 provider 边缘managed-Teams ingress 深度复用本包渲染器但自行解码入站你的解码代码都应严格对齐如下伪逻辑// 出站侧渲染由 copilotkit/channels-teams 完成 // Button onClick{{ id: ck:approve }} value{{ decision: yes }} styleprimary // → Action.Submitdata { ckActionId: ck:approve, value: { decision: yes } } // 入站侧解码消费者必须逐字节一致 function decodeButtonClick(activity) { if (typeof activity.value?.ckActionId ! string) return null; // 普通聊天消息 const id activity.value.ckActionId; const value activity.value.value; // 按钮值可能缺省 const inputs { ...activity.value }; // 含 Input/Select 合并值 delete inputs.ckActionId; delete inputs.value; delete inputs.action; const conversationKey activity.conversation?.id ?? ; return { id, value, inputs, conversationKey }; }验证路径同样明确直接运行本包的合同测试button-action-envelope.contract.test.ts或参照interaction.test.ts中的解码用例端到端验证可用 M365 Agents Playground 在匿名本地模式下启动POST /api/messages默认端口 3978后点击卡片按钮观察等待器恢复。小结CopilotKit 的 Teams HITL 按钮动作信封是一份小而严的线上契约出站用顶层Action.Submit携带data.ckActionIddata.value链接按钮例外走Action.OpenUrl入站以 Message activity 的value原样承载并允许卡片输入合并解码仅凭ckActionId判定、只提取id/value、禁止状态走私、并以conversationKeyOf作为会话键的单一来源。无论是 CopilotKit 自托管适配器还是 managed ingress只有严格对齐这份信封HITL 的按钮往返才能精确、可靠、可恢复。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考