在 Flue 中接入 Intercom:`@flue/intercom` 签名 Webhook 通道实战指南

📅 发布时间:2026/9/17 1:27:28
在 Flue 中接入 Intercom:`@flue/intercom` 签名 Webhook 通道实战指南
在 Flue 中接入 Intercomflue/intercom签名 Webhook 通道实战指南【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flueflue/intercom是 Fluesandbox agent framework官方提供的 Intercom Webhook 入站ingress通道包负责在HEAD /webhook上应答 Intercom 的端点校验、在POST /webhook上接收并验证签名通知然后把经过逐字节验签的通知分发给你的 Agent。读完本文你将掌握如何在 Flue 应用中创建 Intercom 通道、理解其签名校验与通知模型、利用规范化会话身份instanceId把会话派发给 Agent并实现基于notification.id的幂等去重。文中所有结论均可对照 packages/intercom 源码与 examples/intercom-channel 完整示例验证。Flue Channels 与 Intercom 的定位在 Flue 中Channel 是把外部消息平台Slack、Discord、Intercom 等的入站事件转译为 Agent 会话的桥梁。Intercom 通道只解决一个明确问题接收并验证来自 Intercom 的 Webhook 通知。它刻意不承担以下职责源码注释与 README 均有明确声明见 index.ts不负责安装应用、订阅事件、OAuth 授权流程不负责任何出站outboundIntercom 行为例如主动发消息、更新会话不持有投递状态不做去重与排序。这些行为全部由应用层自己掌握因此通道本身是无状态stateless的可以放心部署在任意数量的实例之后。安装与最小接入包名为flue/intercom与flue/runtime配合使用peerDependency运行时依赖 Hono 4.x见 package.json。import { createIntercomChannel } from flue/intercom; export const channel createIntercomChannel({ clientSecret: process.env.INTERCOM_CLIENT_SECRET, // 挂载路径/channels/intercom/webhook webhook({ notification }) { switch (notification.topic) { case conversation.user.created: case conversation.user.replied: console.log(notification.app_id, notification.data.item); return; default: return; } }, });createIntercomChannel(options)会立即做参数校验validateOptions见 index.tsclientSecret必须是非空字符串webhook必须是函数否则直接抛出TypeError。返回的channel对象包含routes通道的路由定义列表只读route()返回一个可挂载的 Hono 子应用通常用app.route(/channels/intercom, channel.route())挂载instanceId(ref)/parseInstanceId(id)会话身份的编码与解析下文详述。通道暴露的两条路由通道固定发布两条路由见 index.ts方法路径用途HEAD/webhook应答 Intercom 的端点校验返回200空响应POST/webhook接收签名通知验签后调用你的webhook处理器对应完整 HTTPS 端点为HEAD /channels/intercom/webhook与POST /channels/intercom/webhook取决于挂载点。在 Intercom Developer Hub 配置时将完整的 HTTPS 端点与期望的 topic 填好即可——安装、OAuth、权限、订阅仍属于应用侧职责。配置参数详解IntercomChannelOptions见 index.ts包含三个字段参数类型必填说明clientSecretstring是Intercom 开发者应用的 client secret用于对请求体做精确逐字节签名校验bodyLimitnumber否请求体最大字节数默认1 MiB1024 * 1024见 webhook.tswebhook(input) result是接收每条已验证通知的回调bodyLimit在校验时必须为正的安全整数否则抛TypeError见 webhook.ts。它的作用是防止超大请求拖垮回调先根据content-length头预判再在流式读取过程中实时累加字节数一旦超限立即中断读取并返回413见 webhook.ts。webhook回调的返回值决定了响应语义serializeHandlerResult见 webhook.ts返回undefined不返回值以200确认返回Response原样透传返回其他 JSON 值以Response.json(...)返回。返回自定义状态码时务必考虑 Intercom 的重试语义——非 2xx 会触发 Intercom 重试谨慎使用。通知模型保留 Provider 原生字段通道刻意不做字段重命名或结构平铺把 Intercom 自己的字段名与嵌套原样透传给你。核心类型IntercomNotification见 index.ts字段如下字段类型说明typenotification_event通知事件类型标记topicstringProvider topic 字符串例如conversation.user.replied不是封闭联合类型未来新 topic 会被原样透传app_idstringIntercom 提供的顶层 workspace 身份idstring \| null通知 id用于应用侧幂等去重ping通知可能为nullcreated_atnumber创建时间戳delivery_attemptsnumber投递尝试次数first_sent_atnumber首次发送时间戳dataIntercomNotificationDataProvider 原生数据信封selfstring \| null可选的 Provider 通知 URL其中data内层IntercomNotificationData见 index.ts把受影响的资源放在data.item下item 的形状由其自身的type字段决定且随 API 版本和 topic 变化因此保留为开放 JSON由应用自行校验。未建模的顶层字段例如新 topic 上的delivery_status、delivered_at、links通过索引签名原样转发保证对未来 topic 的兼容性。签名校验原理先验签、后解析通道最关键的设计是先对请求体做精确的逐字节验签再进行 JSON 解析。完整流程见 webhook.ts内容类型检查content-type必须为application/json否则返回415长度预检content-length非纯数字返回400超过bodyLimit返回413签名提取从x-hub-signature头解析sha140位十六进制parseSignature见 webhook.ts缺失或格式非法返回401流式读体按bodyLimit实时限制读取原始字节readBodyHMAC-SHA1 验证用clientSecret导入 HMAC 密钥importSigningKey见 webhook.ts对原始请求体字节做crypto.subtle.verifyverifySignature失败返回401严格 UTF-8 解码使用fatal: true的 TextDecoder非法编码返回400严格 JSON 解析必须是普通对象isJsonObject递归校验所有值均为有限数值/字符串/布尔/null/对象/数组见 webhook.ts失败返回400信封校验readNotification验证type、topic、app_id、id、created_at、delivery_attempts、first_sent_at、data.item等必需字段的类型合法性见 webhook.ts不合法返回400回调分发将 Hono 上下文c与验证后的notification传给webhook({ c, notification })。由于验签发生在解析之前、且针对的是原始请求字节任何中间环节如代理改写、重复解析都不会破坏签名校验的有效性。HMAC 使用 SHA-1 哈希这与 Intercom 官方x-hub-signature格式sha1一致。幂等与重试语义应用侧职责通道不持有投递状态也不去重、不排序。Intercom 期望回调在5 秒内返回 2xx否则会在约 1 分钟后重试一次通知见 index.ts 的文档注释。因此应用应当快速接收并提交持久化工作不要把慢操作阻塞在回调里用notification.id作为幂等键。在派发时把投递命名为该 id例如idempotencyKey: notification.id这样一次重投递会收敛到最初的那次提交而不是重复创建新会话。注意ping通知的id可能为null示例代码中对此做了显式处理见 examples/intercom-channel/src/channels/intercom.ts仅在id非空时写入notificationId属性。规范化会话身份instanceId 与 parseInstanceIdFlue 需要把每条入站通知映射到唯一的 Agent 实例。Intercom 通道使用instanceId(ref)生成规范化、工作区workspace作用域的身份字符串见 index.tsintercom:v1:workspace:encodeURIComponent(workspaceId):conversation:encodeURIComponent(conversationId)其中IntercomConversationRef见 index.ts由workspaceId取通知顶层的app_id与conversationId组成两个字段都必须是非空字符串assertConversationRef见 index.ts。parseInstanceId(id)是反向解析用正则^intercom:v1:workspace:([^:]):conversation:([^:])$提取并decodeURIComponent还原随后重新编码比对任何不一致都会抛出InvalidIntercomInstanceIdError见 index.ts。需要说明的是instanceId 是规范身份标识不是授权凭据Agent 通常通过创建数据initialData接收结构化事实而非从 id 里解析。实战把 Intercom 会话派发给 Agent仓库中的 examples/intercom-channel 展示了完整链路。需要的环境变量INTERCOM_ACCESS_TOKEN... INTERCOM_CLIENT_SECRET... INTERCOM_WORKSPACE_ID... INTERCOM_REGIONusINTERCOM_REGION支持us、eu、au默认us对应官方 SDK 的IntercomEnvironment见 examples/intercom-channel/src/intercom-client.ts官方 SDK 当前面向 API 版本2.14示例据此固定版本而 Webhook topic item 仍保持 Provider 原生、版本宽容。通道回调的核心逻辑见 examples/intercom-channel/src/channels/intercom.tsexport const channel createIntercomChannel({ clientSecret: requiredEnv(INTERCOM_CLIENT_SECRET), async webhook({ notification }) { switch (notification.topic) { case conversation.user.created: case conversation.user.replied: { const conversationId conversationIdFromItem(notification.data.item); if (!conversationId) return; const conversation: IntercomConversationRef { workspaceId: notification.app_id, conversationId, }; await dispatch(Assistant, { id: channel.instanceId(conversation), initialData: { workspaceId: conversation.workspaceId, conversationId: conversation.conversationId, }, message: { kind: signal, type: intercom.${notification.topic}, body: JSON.stringify(notification.data.item), attributes: { ...(notification.id null ? {} : { notificationId: notification.id }), createdAt: String(notification.created_at), deliveryAttempts: String(notification.delivery_attempts), }, }, }); return; } default: return; } }, });要点dispatch以channel.instanceId(conversation)作为 Agent 实例 id确保同一 Intercom 会话收敛到同一 Agent 实例initialData只在实例创建时记录一次之后被忽略由于 Intercom 会话消息没有单一的纯文本字段data.item以 JSON 字符串原样作为 message body处理器通过useInitialData读取结构化数据、useTool暴露检索工具见 examples/intercom-channel/src/agents/assistant.ts。retrieveConversation返回一个defineTool定义的窄工具调用官方客户端按conversation_id拉取当前会话display_as: plaintext见 examples/intercom-channel/src/channels/intercom.ts。最后在 Hono 应用中挂载见 examples/intercom-channel/src/app.tsconst app new Hono(); app.route(/agents/assistant, createAgentRouter(Assistant)); app.route(/channels/intercom, channel.route()); export default app;这样POST /channels/intercom/webhook收到验签通过的通知后conversation.user.created/conversation.user.replied会被派发到 Assistant 实例其他 topic含未来的新 topic默认忽略且不会被通道阻断。错误类型包导出两个错误类见 errors.tsInvalidIntercomInputError extends TypeError携带field字段标明非法输入的具体字段例如conversation.workspaceIdInvalidIntercomInstanceIdError extends TypeErrorinstance id 格式或内容非法时抛出。小结flue/intercom是一个职责清晰、无状态、对未来 topic 宽容的 Intercom 入站通道它在解析之前对请求体做 HMAC-SHA1 精确验签固定暴露HEAD/POST /webhook两条路由把 Provider 原生通知结构原样交给应用用intercom:v1:workspace:...:conversation:...的规范化身份把会话绑定到 Agent 实例并把安装、OAuth、订阅、出站行为、去重等职责全部留给应用侧。配合 examples/intercom-channel 示例和flue/runtime的dispatch你可以快速搭建一个生产可用的 Intercom 支持助手验签入站、按 topic 派发、以notification.id幂等、用工具按需拉取会话上下文。【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考