- 后端
- 前端
- 数据分析
- 数据可视化
【免费下载链接】growthbook
Open Source Feature Flags, Experimentation, and Product Analytics
本文以 GrowthBook 后端仓库中的 Slack 服务文档 为核心,完整讲清自托管 GrowthBook 时如何配置 Slack 应用(环境变量、事件/交互回调地址、Bot Scopes 与订阅事件),并结合 services/slack/ 目录下的源码,深入解析请求签名校验、事件白名单、账户绑定、组织路由、线程租约、限流重试与队列恢复等底层机制,帮助读者既会配置、也能排查该集成的实现细节。
一、自托管环境下的应用配置
Slack 集成依赖 Workspace OAuth 模式,需要配置三组环境变量(这些键在 util/secrets.ts 中被读取):
| 环境变量 | 作用 |
|---|---|
SLACK_CLIENT_ID | Slack 应用的 OAuth Client ID |
SLACK_CLIENT_SECRET | OAuth Client Secret |
SLACK_SIGNING_SECRET | 校验入站 Slack 请求的签名(HMAC-SHA256) |
自建应用的回调地址遵循一个统一规则:OAuth 重定向使用APP_ORIGIN,而事件(Events)与交互(Interactions)回调使用API_HOST。对于已有的 Slack 应用,需要配置:
- Events Request URL:
https://YOUR_API_HOST/integrations/slack/events - Interactivity Request URL:
https://YOUR_API_HOST/integrations/slack/interactions - Bot 事件订阅:
app_mention、message.im、app_home_opened、link_shared
创建应用后,填入凭据、重启 GrowthBook,再到 Slack 的 Event Subscriptions 设置页核实 Events Request URL 是否可达——Slack 必须能通过 HTTPS 访问到API_HOST。GrowthBook 的自托管安装清单(setup manifest)会一次性注册 assistant 事件、interactivity 和 Messages tab;对于 Cloud 版,则使用https://app.growthbook.io/integrations/slack作为 OAuth 重定向地址、https://api.growthbook.io作为上述两个回调地址的 API host,并在 Slack 应用后台开启 public distribution 以便其他工作区通过 GrowthBook 的 OAuth 连接流程安装。
事件订阅的最小化原则
文档明确列出了不应订阅的内容:不要为当前版本添加channels:history、groups:history、mpim:history、message.channels、message.groups、message.mpim。原因是频道消息只以app_mention事件形式被接受(包括线程内的 @提及),其余频道消息在查库或建任务之前就会被丢弃——即使旧版应用配置仍在投递它们。DM 消息无论是否 @提及都会接受;机器人自己的消息和edit等消息 subtype 一律忽略。app_home_opened仅用于刷新 DM 引导提示,并不会开启一段 assistant 对话。
Bot 所需的 scopes 及其用途如下(完整继承自原文档):
| Scope | 用途 |
|---|---|
chat:write | Assistant 回复、私密账户链接提示、通知 |
files:write | 通知图表图片 |
channels:read、groups:read | 通知频道的选择与校验 |
channels:join | 加入被选为通知目标的公共频道 |
assistant:write | Messages tab 中的建议提示(suggested prompts) |
im:history | 用户直接发给机器人的消息 |
app_mentions:read | 频道中的显式 @提及 |
links:read、links:write | 接收匹配的链接并回发自定义 unfurl 的权限 |
源码中的单一事实来源
这些 scopes 与事件列表并非只写在文档里,而是集中在 packages/shared/src/slack-integration.ts 中,供 OAuth 连接流程和自托管安装清单共用:
// packages/shared/src/slack-integration.ts (L1-L21) export const SLACK_BOT_SCOPES = [ "chat:write", "files:write", "channels:read", "groups:read", "channels:join", "assistant:write", "im:history", "app_mentions:read", "links:read", "links:write", ] as const; export const SLACK_BOT_EVENTS = [ "app_mention", "message.im", "app_home_opened", "link_shared", ] as const;同文件还提供missingSlackBotScopes(granted):按逗号拆分已授予的 scopes 后与上表求差集。这意味着如果旧工作区安装缺少这些授权,前端可以检测出缺失项并提示重新连接(reconnect)。
关于link_shared需要说明现状:manifest 会把APP_ORIGIN主机名注册为 unfurl 域,Slack 会对匹配的 URL 过滤并只发送链接元数据而非消息文本;但当前后端对这些事件只做确认(acknowledge),尚未生成预览——权限与订阅是为未来的 unfurl 处理器预留的。修改 unfurl 域需要重新安装应用。
二、入站请求的两道关卡:签名校验与事件白名单
1. HMAC 签名校验
Slack 使用SLACK_SIGNING_SECRET对v0:<timestamp>:<raw body>做 HMAC-SHA256 签名,以X-Slack-Signature头发送。实现见 slackRequestSignature.ts:
// L3, L25-L36(节选) const MAX_AGE_SECONDS = 5 * 60; // 时间戳偏差超过 5 分钟直接拒绝(防重放) // 对 `v0:timestamp:rawBody` 计算 HMAC,与 X-Slack-Signature 用 timingSafeEqual 比对源码注释特别强调:参与签名的 body 必须是 Slack 发送的原始字节,而不能是解析后再序列化的 JSON——这是排查签名校验失败时的常见坑。
2. 事件白名单
Assistant 侧的入口解析在 slackAssistantEvents.ts 中,用 zod 的discriminatedUnion只接受两类事件(L23-L29):
type: "app_mention"的消息事件;type: "message"且channel_type: "im"的 DM 事件。
随后还有三道硬过滤(L40):带bot_id的消息(机器人自己发的)、带subtype的消息(如 edit)、发送者即 bot 用户本身的消息,全部返回null丢弃。这与前文"不订阅历史类 scope、频道消息仅接受 @提及"的策略在代码层完全对齐。
三、账户绑定与组织路由
1:1 连接模型
每个 Slack 工作区只连接一个 GrowthBook 组织,反之亦然。这一策略由 SlackWorkspaceConnectionModel.ts 中的两个唯一索引落地:slack_one_org_per_workspace与slack_one_workspace_per_org。两条 OAuth 安装路径都会拒绝冲突连接;重复连接同一对(工作区+组织)只会刷新凭据。索引在启动时后台创建,只负责封堵并发竞态;当索引建不出来时(已有冲突连接,或 Cosmos DB 只在空集合上建唯一索引),错误只记录日志,连接时仍执行冲突检查——最终一个工作区出现两条连接时,其 Slack 事件到达时会被拒绝,直到多余的连接被断开。若将来要支持共享工作区,需要移除该校验、显式删除这两个策略索引并扩展工作区解析器。
私密链接与 15 分钟有效期
用户通过在 DM 中发送link account,或在频道中 @机器人加这段文本,获得一个私密签名链接(ephemeral 消息,仅本人可见)。链接生成逻辑在 slackLink.ts:
// L6, L15-L24 const LINK_STATE_MAX_AGE_MS = 15 * 60 * 1000; // 15 分钟 export function buildSlackLinkUrl(identity) { const state = signState({ ...identity, nonce: randomBytes(12)..., createdAt: Date.now() }); return `${APP_ORIGIN}/integrations/slack/link?state=${encodeURIComponent(state)}`; }要点:
- 该链接需要登录 GrowthBook才可用,过期后必须重新触发;
- 同意页(consent page)会展示 Slack 工作区/用户、已登录的 GrowthBook 账户和将连接的 Organization,确认后才生效;
- 每个组织内一个签名 token 只能成功使用一次;对已成功同意的重试是幂等的,失败或已断开的同意需要重新拿链接;
- 替换已绑定的 GrowthBook 账户时,用替换账户登录后打开新的私密链接并确认即可;
- 明确的
link account请求不会开启 assistant 回合;绑定完成后需回到 Slack重新发送原问题,绑定不会自动恢复问题或撤掉私密提示。
目标解析:六重校验
slackIdentity.ts 中的resolveSlackAssistantTarget是每回合的身份闸口,它按顺序检查并给出精确的失败原因(L127-L207):
no_connection:工作区未连接 GrowthBook;no_bot_token:连接存在但 bot token 解密失败,提示管理员重装应用;not_linked:该 Slack 身份在对应组织中没有账户链接——此时返回botToken,让调用方以 ephemeral 消息发送带签名链接的提示;not_a_member:已绑定的 GrowthBook 账户失去了组织访问权;assistant_disabled:工作区级 assistant 开关被显式关闭(connection.assistantEnabled === false);license_unavailable:Agenda 任务跳过了加载 license 的鉴权中间件,此处手动licenseInit失败时的兜底。
通过后返回{ context, userId, linkId, organizationId, botToken }——所有后续操作都运行在被链接用户当时的组织权限上下文中。文档中"assistant 跟随组织的 AI 设置,除非其 Slack 工作区开关被显式设置;组织的 AI 访问、用量限制和链接用户权限始终生效"即在此处体现。
会话身份绑定
slackTaskSafety.ts 定义了会话 ID 的构造(L32-L41):conv_slack_前缀 + 对[teamId, channelId, rootTs, organizationId, slackUserId, userId, linkId]的 SHA-256。每位 Slack 参与者拥有独立会话,且绑定到 Slack 身份、GrowthBook 账户、组织和当前 link 标识符——因此每次替换绑定都会生成新 link 标识,旧会话和未决审批全部失效,即使重新绑定到同一账户也无法复用。若工作区改连到其他组织,在旧线程里回复会在新组织开启新会话,而旧组织的审批因会话不再匹配而被拒绝;成员资格、配置与权限在执行动作时还会再查一遍。
前端侧入口
个人账户菜单的My Slack links页面(经 slack-integration.router.ts 的GET/DELETE /links等路由)列出当前用户在所选组织中的链接并允许断开;这些操作不需要集成管理员权限。Workspace OAuth 连接是绑定、DM 与频道 @提及的权威来源,包括没有任何通知频道的全新安装;在 Slack 里直接邀请机器人进频道即可对话。通知订阅不限制 assistant 的访问,删除通知订阅也不会中断 assistant 对话。
四、Assistant 回合:占位消息、线程租约与 15 分钟超时
slackAssistant.ts 是回合执行的主体,其流程与文档描述一一对应:
- 剥离 @提及:
stripBotMention(L70-L82)只吸收提及及其周围空白,其余空白原样保留,以保护引用的值与粘贴的代码。 link account快捷命令:文本恰为link account时直接回发私密链接(L173-L182),不进入 AI。- 先发占位消息:回复前先在根线程发出
_Thinking…_(L59、L262-L269),随后用chat.update原地替换为答案;若update失败则退化为线程内新消息。 - 线程串行执行:
withThreadTurn(L113-L152)通过slackTaskClaims模型对thread:<sha256(teamId,channelId,rootTs)>键获取租约。从 SlackTaskClaimModel.ts 可确认租约参数:
const THREAD_LEASE_TTL_MS = 2 * 60 * 1000; // 2 分钟过期 export const THREAD_LEASE_RENEW_MS = THREAD_LEASE_TTL_MS / 4; // 30 秒续租持锁期间每 30 秒续租,回合结束释放;拿不到锁时抛SlackThreadBusyError(携带首次尝试发出的占位消息ts),Agenda 任务在 5 秒后重排并复用原占位消息,因此用户在第一个问题还在回答时快速发第二条消息,会得到确认而不是重复的"Thinking…"。 5.15 分钟回合上限:MAX_TURN_MS = 15 * 60 * 1000(L64)。到达期限或续租发现租约被接管时,AbortSignal 触发:AI 流被中止、跳过最终会话保存、占位消息被替换为超时提示,续租同时停止——即使回合函数永不返回,租约也会自然失效。已派发的 mutation 仍可能完成,其永久 action claim 防止重放。
多 worker 场景下的安全保障来自 services/queueing.ts:Agenda 实例配置defaultLockLifetime: 10 * 60 * 1000(10 分钟),所有处理器(Slack 的也包括在内)都经过共享队列包装器;按 README 的说明,该包装器每 9 分钟续租一次 job 锁,因此长回合不会被第二个 worker 拾取。worker 中途死亡(部署或崩溃)时续租停止,线程的下一回合会在 2 分钟内接管租约,无需管理员干预。
审批卡片与永久 action claim
mutation 类结果不直接执行,而是渲染为带 Confirm/Cancel 按钮的审批卡片(postPendingApproval,L333-L423):标题截断到 150 字符、正文按 Slack 3000 字符限制截断、按钮value里编码{ c: conversationId, a: actionId, t: threadTs };若请求带ignoreWarnings,卡片标题下会显式警告"确认后将无视 GrowthBook 的警告继续"。
点击处理在handleSlackAssistantConfirmation(L431-L638),其防重放设计对应 README"Queue recovery"一节的最后一段:
- 会话归属校验(L473-L491):把点击者重新解析出的会话 ID 与卡片绑定的
conversationId比对,不匹配则回复"This action isn't yours to confirm."; - 陈旧卡片退役(L498-L527):若会话的
pendingAction已不是当前actionId,卡片会被改写为"Replaced by a newer request."并以 ephemeral 消息告知; - 永久 claim 时机(L535-L561):
beforeResolvePendingAction钩子先复核链接/权限指纹未变化,然后在真正派发 API 调用之前用claimOnce获取action:<sha256(teamId, orgId, conversationId, actionId)>声明。预检失败不获取 claim,原审批按钮保持可用;一旦 claim 到手,即使随后崩溃导致结果不确定,也不会重放。
五、限流、去重与队列恢复
Slack Web API 限流
slackWebApi.ts 的两个常量与 README 完全一致:
const SLACK_MAX_RATE_LIMIT_RETRIES = 3; const SLACK_MAX_RATE_LIMIT_WAIT_MS = 60_000;重试循环(L75-L110)的语义是:遇到 HTTP 429 时读取Retry-After头;有合法值则按原样等待——注释明确写道"从不缩短 Slack 的冷却来迁就 worker 的等待预算";值缺失或非法则按1000 * 2 ** retry指数退避(首档 1 秒)。累计等待超过 60 秒或重试超过 3 次即抛出SlackRateLimitError。文档强调:回复投递的重试耗尽会让 assistant 任务以失败结束,而不是静默成功或重放 AI 回合/ mutation——SlackRateLimitError在slackAssistant.ts中被显式 rethrow(L325、L626)以保证这一点。
投递去重
- 事件与交互请求只有在 Agenda接受任务之后才向 Slack 返回 ack;
- 唯一的投递索引 + insert-only upsert 保留已完成投递的身份标识,保留期与 Agenda 的常规清理周期一致(7 天);重复投递永远不会重新调度已完成的任务;
- 投递索引在启动时尽力构建:DocumentDB 5.0 之前和 Cosmos DB 无法构建该索引,此时同一事件的两个投递若同时到达,可能各跑一个回合(README 明确声明了这一边界);
- 数据库故障时接口返回503,让 Slack 自行重试;
- 按钮类投递按 Slack 点击时间戳(
interactionTs)去重:一次全新的点击可以重试失败的访问/用量检查,而永久 action claim 只在这些检查通过后才获取。
消息与通知的通道能力
同一slackWebApi.ts还封装了通知侧用到的 API:chat.postMessage/chat.postEphemeral/chat.update、conversations.list(公共/私有频道,limit: 200分页)、conversations.join(幂等加入公共频道),以及files.getUploadURLExternal→ 直传 →files.completeUploadExternal的三段式图片上传(L266-L329)。源码注释解释了为什么"上传时即分享":引用尚未处理完的私有文件会失败,且频道成员可能根本看不到它;分享消息优先用 blocks 承载 caption,被 Slack 拒绝时退回initial_comment,保证卡片不降级为纯文本。这些正是 scope 表中files:write、channels:read、channels:join三个权限的实际落点;通知投递与 assistant 开关相互独立(见 deliverSlackNotification.ts)。
六、AI 助手与 Messages tab 的实现细节
Assistant 的"人格"由 slackAgent.ts 定义:在通用 agent 配置基础上叠加 Slack 专用系统提示(SLACK_REPLY_GUIDANCE),核心要求包括——用产品语言而非 API 细节作答、实体名一律生成同源于相对路径的 markdown 链接(由toSlackMrkdwn统一改写为绝对 Slack 链接)、指标链接按 id 前缀区分/fact-metrics/<id>与/metric/<id>。工具集复用buildCoreAgentTools,与网页端 agent 共享能力边界。
Messages tab 侧的实现在 slackAppHome.ts:zod schema 严格匹配type: "app_home_opened"且tab: "messages"的事件(这正是文档要求"迁移旧应用时删除assistant_thread_started订阅、改用app_home_opened"的原因),处理函数每次打开都用assistant.threads.setSuggestedPrompts刷新三条静态建议——"Link my account"、"Running experiments"、"Feature Flags"——绝不发起 AI 回合,也不发送欢迎消息。slackWebApi.ts 中setSlackSuggestedPrompts的注释点出一个 API 细节:agent_view的 prompt 属于 Messages tab,携带thread_ts会静默失败,因此建议提示必须省略该字段——真实对话仍从用户消息开始,并沿用既有的账户与 AI 访问检查。
小结
GrowthBook 的 Slack 集成可以用一句话概括其设计取向:最小事件面 + 强身份绑定 + 全链路防重放。配置层面只需三个环境变量、两个回调地址和一组精简 scopes(由 shared/slack-integration.ts 统一维护);实现层面则以 5 分钟签名时间窗、app_mention/im白名单、15 分钟过期绑定链接、组织 1:1 唯一索引、线程级 2 分钟租约、按点击时间戳去重的审批卡片和"失败优于重放"的限流策略,把 Slack 事件这种外部、不可信、可能重复的输入,约束在安全可控的回合模型之内。排查线上问题时,建议按"签名 → 事件白名单 → 工作区连接 → 用户链接 → 租约/claim"的顺序对照本文各节的文件定位原因。
- 后端
- 前端
- 数据分析
- 数据可视化
【免费下载链接】growthbook
Open Source Feature Flags, Experimentation, and Product Analytics
相关推荐
Serverless Framework 实战:为 AWS Lambda 配置 SQS 队列事件(含批处理、事件过滤与并发控制)
Serverless Framework 实战:为 AWS Lambda 配置 SQS 队列事件(含批处理、事件过滤与并发控制) 本指南聚焦 Serverles
开发工具CLI云原生后端cann/asc-devkit HCCL通信Tiling接口
v1版本TilingData(废弃)<a name="ZH CN_TOPIC_0000001940699904" </a !NOTE 说明 该结构体废弃,并将在
人工智能深度学习算子库CANNAscendMastra 集成 Turbopuffer 向量存储指南:从接入配置到过滤查询的完整实战
Mastra 集成 Turbopuffer 向量存储指南:从接入配置到过滤查询的完整实战 Turbopuffer 是一款以高吞吐、低延迟为特点的托管向量数据库,
人工智能Agent 框架AI AgentRAG后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考