Slack × Claude Managed Agents:用无状态 Webhook 桥接器把 CMA Agent 接入 Slack 线程
在 Slack 里 @ 提及一个 Claude Managed Agent(CMA),并让 Agent 的回复直接出现在对应线程(thread)中——这是本指南围绕的核心场景。本指南以仓库中 managed_agents/slack 目录下的 README 为骨架,逐层展开它的架构设计(Slack 事件 + Anthropic 会话回调两条单向链路)、sessions.create 携带的 metadata 路由机制,以及从本地快速启动到生产化改造的完整实操步骤,同时结合 skill.md 与各源码文件讲解其中的关键陷阱与排查手段。读完你将能独立搭建一个"Slack 消息进、Agent 回复出"的无状态桥接服务,并具备自行调试与扩展的能力。
整体架构:两个 webhook、一条无状态桥
仓库中 skill.md 将这套系统概括为"one bridge, two webhooks"。它与常见的"双向长连接 Agent"完全不同:Claude 会话在 Anthropic 基础设施上异步运行,因此 Slack 与桥接服务之间、Anthropic 与桥接服务之间各有一条单向信号链路:
Slack @mention ──▶ /slack/events ──▶ sessions.create (+ metadata) ──▶ 200
│
Claude runs to idle on Anthropic infra
│
/cma-webhook ◀── session.status_idled ◀───────┘
│
└──▶ sessions.retrieve → read metadata → chat.postMessage
理解这套设计要抓住三个要点:
- Slack → 桥(
/slack/events)在用户@机器人或私聊时触发。事件载荷携带channel、ts、thread_ts、user、text等上下文。 - Anthropic → 桥(
/cma-webhook)在 CMA 会话转入 idle(空闲)或 terminated(终止)时触发。载荷只携带 CMA session ID。 - 两条链路都不携带 Agent 的输出。它们只是携带 ID 的"门铃信号",真正的数据(metadata、回复文本)需要桥接器主动拉取。
webhook 是门铃,不是投递员
Anthropic 的 session.status_idled 载荷被刻意设计得很薄——本质上只有 {type, id}。收到信号后,桥接器需要主动调用 sessions.retrieve(id) 拿回 metadata,再通过 sessions.events.list(id) 拉取输出。即"推送信号,拉取数据"。这一设计体现在 src/cma-webhook.ts 的处理逻辑中。
metadata 就是全部的路由状态
这是整套设计的灵魂。在 kickoff 阶段,桥接器把 Slack 路由信息写入 CMA 会话:
const session = await anthropic.beta.sessions.create({
agent: CLAUDE_AGENT_ID,
environment_id: CLAUDE_ENVIRONMENT_ID,
metadata: {
slack_channel: m.channel,
slack_thread_ts: m.thread_ts,
slack_team: m.team,
},
});
代码见 src/agent.ts。当若干秒后 idle webhook 到达、载荷里只有 session ID 时,桥接器调用 sessions.retrieve(id),从返回的 session.metadata 中读回 slack_channel、slack_thread_ts,再用 chat.postMessage 精确投递到原始线程。桥接器自身不保存任何会话状态,这就是它无状态的原因。
面向 Slack 3 秒确认窗口的取舍
Slack 对任何未在 3 秒内返回 2xx 的事件会进行重试。桥接器必须在 CMA 会话结束前返回,因此处理流程被设计为:验证签名 → 按 event_id 去重 → 不 await kickoffAgentSession()(fire-and-forget)→ 立即返回 204。这一时序约束在 src/slack-events.ts 中有完整体现。
目录结构与依赖概览
managed_agents/slack/README.md 给出了清晰的模块划分:
| 文件 | 职责 |
|---|---|
| setup/create-agent.ts | 一次性操作:agents.create + environments.create,打印 ID |
| src/main.ts | Bun 服务入口与路由分发 |
| src/slack-events.ts | 校验 Slack 签名、处理 url_verification、fire-and-forget 启动会话 |
| src/agent.ts | sessions.create + user.message,携带路由 metadata |
| src/cma-webhook.ts | beta.webhooks.unwrap → 按 metadata 过滤 → chat.postMessage |
| skill.md | 安装走查、常见陷阱与调试指引 |
依赖方面,package.json 声明了 @anthropic-ai/sdk(要求 ≥ 0.95.1,因为 beta.webhooks.unwrap 与 CMA Beta API 需要该版本)与 @slack/web-api(用于 chat.postMessage)。完整的依赖与 npm scripts(dev / start / setup)见 managed_agents/slack/package.json。
服务入口与环境变量检查
src/main.ts 使用 Bun.serve 暴露三个端点:
GET /→{ status: "ok" }健康检查;POST /slack/events→ 转发给handleSlackEvents;POST /cma-webhook→ 转发给handleCmaWebhook。
服务启动时会强制校验五个环境变量,任一缺失即 FATAL 退出:
const PORT = Number(process.env.PORT) || 3000;
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
for (const v of [
"SLACK_SIGNING_SECRET",
"SLACK_BOT_TOKEN",
"ANTHROPIC_WEBHOOK_SIGNING_KEY",
"CLAUDE_AGENT_ID",
"CLAUDE_ENVIRONMENT_ID",
]) {
if (!process.env[v]) {
console.error(`FATAL: ${v} is required`);
process.exit(1);
}
}
其中 SLACK_SIGNING_SECRET / SLACK_BOT_TOKEN 来自 Slack 应用,ANTHROPIC_WEBHOOK_SIGNING_KEY 来自 Anthropic Console 的 Webhooks 配置,CLAUDE_AGENT_ID / CLAUDE_ENVIRONMENT_ID 来自 bun run setup 的一次性创建脚本。
从零配置的完整流程
一次性创建 Agent 与 Environment
首次使用前需要运行 setup/create-agent.ts,它调用 Beta API 分别创建 cloud 环境与 Agent,并把两个 ID 打印到终端供写入 .env.local:
const env = await anthropic.beta.environments.create({
name: `slack-bridge-${Date.now()}`,
config: { type: "cloud", networking: { type: "unrestricted" } },
});
const agent = await anthropic.beta.agents.create({
name: "Slack Assistant",
model: "claude-opus-4-7",
system:
"You are a helpful assistant embedded in Slack. Keep replies concise and conversational — they are posted as thread replies. Use plain text or Slack mrkdwn (e.g. *bold*, `code`); avoid Markdown headers.",
tools: [{ type: "agent_toolset_20260401", default_config: { enabled: true } }],
});
console.log("\nAdd to .env.local:");
console.log(`CLAUDE_ENVIRONMENT_ID=${env.id}`);
console.log(`CLAUDE_AGENT_ID=${agent.id}`);
注意系统提示词专门针对 Slack 场景做了约束:回复保持简短、口语化,因为会作为线程回复展示;使用纯文本或 Slack mrkdwn 语法而非 Markdown 标题。agent_toolset_20260401 是该仓库所用 SDK 版本下的 Agent 工具集类型标识,default_config.enabled: true 表示默认启用。
本地开发检查清单
按 skill.md 的 Local dev checklist 顺序执行(顺序决定成败):
ngrok http 3000→ 记录公共 URL(后续两个 webhook 的 Request URL 都要用它)。bun run setup→ 把CLAUDE_AGENT_ID/CLAUDE_ENVIRONMENT_ID复制进.env.local。之后再粘贴 Slack 密钥时不要覆盖它们。- Slack App → OAuth & Permissions → Bot Token Scopes 添加
app_mentions:read、chat:write(若需私聊还加im:history)→ Install to Workspace → 复制xoxb-…写入SLACK_BOT_TOKEN。 - Slack App → Basic Information → 复制 Signing Secret →
SLACK_SIGNING_SECRET。 - Anthropic Console → Manage → Webhooks:填入
<url>/cma-webhook,订阅session.status_idled+session.status_terminated→ 复制whsec_…→ANTHROPIC_WEBHOOK_SIGNING_KEY。必须与 API key 处于同一 workspace。 bun run dev— 服务必须在步骤 7 之前启动。- Slack App → Event Subscriptions → 开启 → Request URL 填
<url>/slack/events→ 显示 Verified ✓ → 添加 bot 事件app_mention→ Save Changes → 按提示重装应用。 - 在 Slack 中
/invite @your-bot拉进频道,然后@your-bot hello验证。
项目入口文档建议用更简洁的方式:在目录下执行三条命令后,直接问 Claude "walk me through setting this up",由 Claude 读取 skill.md 并按照真正能跑通的顺序驱动整个配置过程:
cd managed_agents/slack
bun install
claude
两个端点各自的请求处理
Slack 事件端点(src/slack-events.ts)的处理顺序是:
- 读取原始 body 并做签名校验:计算
v0:+ timestamp + rawBody 的 HMAC-SHA256 并与x-slack-signature做timingSafeEqual常量时间比较,同时校验时间戳在 ±5 分钟容差内(TOLERANCE_SEC = 5 * 60),失败返回 401; - 若
payload.type === "url_verification",原样返回challenge完成 URL 验证; - 非
event_callback直接 204; - 用
event_id做幂等去重(Slack 重试会复用同一event_id); - 判定
app_mention(频道@提及)或im私聊且非 bot 回声且有文本; - 用
stripMention去掉文本中的<@...>提及前缀; - fire-and-forget 调用
kickoffAgentSession(.catch记录错误)并立即 204。
Anthropic webhook 端点(src/cma-webhook.ts)的处理顺序是:
- 用
anthropic.beta.webhooks.unwrap(rawBody, { headers: Object.fromEntries(req.headers) })完成 HMAC 签名验证与解析。注意unwrap()需要普通字符串映射表而非 fetch 的Headers对象,因此这里做了Object.fromEntries转换(@anthropic-ai/sdk≥ 0.95.1 才支持); - 按顶层
event.id幂等去重(Anthropic 重试会复用同一event.id); - 只处理
session.status_idled与session.status_terminated两种类型; - retrieve-then-filter:先
sessions.retrieve(id)(任何异常一律 204 静默丢弃),再检查 metadata 中是否存在slack_channel/slack_thread_ts,没有就 204 退出——过滤必须在调用events.list之前完成; terminated事件向原线程发一条警告消息;- 正常 idle:用
for await迭代sessions.events.list(id)(分页对象自动翻页),收集所有agent.message事件中的textblock,拼接后通过slack.chat.postMessage回帖到线程。
新手最容易踩的坑(Gotchas)
skill.md 整理了文档里不明显、却最耗时的一批坑,配置时请逐条核对。
用开发者沙箱而不是公司工作区
Slack 的 Developer Program Sandboxes 提供免费的 Enterprise Grid 测试组织:有管理员权限、假用户/假频道,不会把半成品 bot 装进生产环境。注意这不是 "agent quickstart",而是 Developer Program → Sandboxes。流程:加入 Slack Developer Program → 邮件激活 → 接受条款 → 在面板 Provision Sandbox(可选空沙箱或预置假用户/频道)→ 从邮件邀请完成设置(你成为 Primary Org Owner)→ 在沙箱内创建至少一个 workspace → 把应用建在沙箱 workspace 上开发。
OAuth scope ≠ 事件订阅
在 OAuth & Permissions → Bot Token Scopes 里加 app_mentions:read 只是授予了"看到提及"的权限,不会让 Slack 投递事件。还必须去 Event Subscriptions 页面打开开关、填 Request URL,并在 "Subscribe to bot events" 里添加 app_mention。这是两个独立页面,漏掉第二个会"零投递且没有任何报错"。
xoxb- 与 xapp-:两个 token、两个页面,极易拿错
| Token | 前缀 | 所在页面 | 用途 |
|---|---|---|---|
| Bot User OAuth Token | xoxb- |
OAuth & Permissions(安装后出现) | chat.postMessage —— 这才是你要的 |
| App-Level Token | xapp- |
Basic Information → App-Level Tokens | 仅 Socket Mode WebSocket 使用 |
拿 xapp- token 调 chat.postMessage 会以 invalid_auth 失败。xoxb- token 只有在你至少添加了一个 bot scope 并且点击了 Install/Reinstall to Workspace 之后才会出现。
保存 Request URL 前桥接服务必须在线
保存 Event Subscriptions URL 会立即触发一次 url_verification POST。如果隧道上没有服务在监听,Slack 会报 "Your URL didn't respond" 且 URL 无法保存。因此先 bun run dev,再粘贴 URL。
Anthropic webhook 是按 workspace 隔离的
在 Console 注册的端点只会收到同一 workspace 内会话的事件。若 ANTHROPIC_API_KEY 属于 workspace A、但端点在 workspace B 注册,结果是"零投递、无报错"。注册 Webhooks 页面时的工作区选择器必须与 API key 所在 Workspace 列保持一致。
workspace 级 webhook 会收到该 workspace 的所有会话
如果 Anthropic workspace 与其他 Agent、脚本或同事共享,那么 workspace 内每一次 session.status_idled 都会打到你的端点。处理器的应对策略:
- 永远先 retrieve 再过滤。
sessions.retrieve(id)→ 检查自己的slack_channelmetadata key → 不存在就 204 退出。必须在events.list()或任何其他工作之前完成,否则无关会话会在处理器深处抛 404。 - 对
sessions.retrieve捕获 404/403——同一 workspace 内由其他 API key 创建的会话,你的 key 读不到。 - 生产环境使用专用 Anthropic workspace。每个无关会话都要消耗一次
retrieve()调用来丢弃;只含本 Agent 会话的 workspace 则完全避免这种开销。
unwrap() 需要普通 header 映射
client.beta.webhooks.unwrap(body, {headers})(SDK ≥ 0.95.1)需要 Record<string, string>,而不是 fetch 的 Headers 对象。请传 Object.fromEntries(req.headers)。
event.id 就是幂等键
Anthropic 会用相同的顶层 event.id 重试失败投递;Slack 则用 body 内相同的 event_id 重试。两者都要去重。一旦处理或忽略某事件就返回 2xx——返回任何其他状态都会触发重试,而 Anthropic 约连续 20 次失败会自动禁用你的端点。
静默失败的调试路径
skill.md 给出了一张非常实用的"对症下药"表,核心思路是先判断断点在哪条链路:
- 桥接日志里完全没有内容 → Slack 根本没到达你。用
curl localhost:4040/api/requests/http查看 ngrok 的请求日志。看不到/slack/eventsPOST = Event Subscriptions 没保存,或 Socket Mode 被打开了。 [agent] kickoff已打印但没有回复 → 看 ngrok 日志里有没有/cma-webhookPOST:没有,则是 Anthropic workspace 不匹配或端点未保存;出现 401,是ANTHROPIC_WEBHOOK_SIGNING_KEY不匹配;返回了 204 但 Slack 没发帖,则是SLACK_BOT_TOKEN错误(检查是否误填xapp-)或缺chat:writescope。400 Invalid agent ID→.env.local里还是agent_...占位符。从bun run setup的输出重新粘贴真实 ID。chat.postMessage报not_in_channel→ 先把 bot/invite进频道。
生产化改造方向
skill.md 的 Production notes 与 CLAUDE.md 共同勾勒出从本地 demo 走向生产的四条路径:
- 替换 ngrok:用真正的部署(网关/公网入口)顶替隧道,业务代码无需任何改动。
- 把内存幂等集替换为共享存储:当前两个端点分别用内存
Set(seenEventIds)做去重,多实例部署会失效,需要换成 Redis / 数据库实现跨实例幂等。 - 多 workspace(分布式)Slack 应用:把静态
SLACK_BOT_TOKEN换成按metadata.slack_team键控的 per-team 令牌存储,并补充 Slack OAuth 授权流程。 - 多轮对话记忆:每次
@都会创建全新的 CMA 会话(跨轮无记忆)。要做线程内连续对话,应缓存thread_ts → session_id映射,后续消息改用sessions.events.send追加而非重复sessions.create。
此外,CLAUDE.md 还列举了在基础桥接之上可选的扩展方向,均可通过编辑 setup/create-agent.ts 或 src/agent.ts 实现:
- 挂载 GitHub 仓库:在
sessions.create的resources中加入{type: "github_repository", ...}; - 接入 MCP 工具:给 Agent 配
mcp_servers+mcp_toolset,让 Agent 不仅能回复、还能"动手"(如 Slack/GitHub MCP),凭据走 vault,通过vault_ids挂到会话; - Outcomes 评估循环:用
user.define_outcome事件替代user.message,实现按 rubric 评分的迭代; - 多 Agent(Multiagent):在 Agent 上配置
multiagent: {type: "coordinator", agents: [...]}形成协调者 + 子 Agent 编队; - 跨会话记忆:通过
resources: [{type: "memory_store", ...}]持久化; - 自定义工具:通过
agent.custom_tool_use/user.custom_tool_result在宿主侧执行。
小结
本案例展示了一个值得复用的集成范式:异步后台 Agent 与即时消息平台之间,靠"两个 webhook 信号 + 一次会话 metadata 存取"完成无状态桥接。核心不变量有三条——把路由状态写进 CMA 会话的 metadata(slack_channel、slack_thread_ts、slack_team);对 workspace 级 webhook 一律"先 retrieve 再按 metadata 过滤";对 Slack 3 秒确认窗口采用签名校验 + 幂等去重 + fire-and-forget 的响应策略。按仓库内 README.md、skill.md 与 src 下源码的组合,你可以快速复刻、调试并扩展出自己的 Slack × CMA 助手。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00