
1. 项目缘起与核心思路拆解1.1 为什么要在 Slack 里塞一个“会主动干活”的 AI团队协作工具里最不缺的就是消息最缺的是“有人主动把事办了”。Slack 作为很多技术团队的主战场每天产生大量讨论、决策、待办和知识碎片但这些东西往往散落在各个频道里过两天就沉底了。company-brain 这个项目的切入点很直接与其让成员手动去翻记录、整理纪要、同步进度不如在 Slack 里放一个常驻的 AI 代理让它主动监听、理解、响应甚至在没有被 的情况下也能判断“这件事该我出手了”。这个“主动干活”的能力核心依赖两个东西一是 Slack 的事件订阅机制让 AI 能实时收到频道里的消息二是 MCPModel Context Protocol工具调用协议让 AI 不只是聊天而是能真正去操作外部系统——查数据库、调 API、写文档、发通知。Cloudflare Workers 则提供了轻量、低延迟、按需伸缩的运行环境不用自己维护服务器部署一个 Worker 就能让 AI 代理 7×24 小时在线。适合谁来参考这个项目三类人最值得看一是想给团队内部工具加 AI 能力的全栈开发者二是正在探索 MCP 协议落地场景的工程师三是想用 Cloudflare Workers 做轻量后端但不知道从哪下手的人。哪怕你之前没接触过 Slack Bot 开发跟着思路走也能理解整个链路是怎么串起来的。1.2 整体架构Slack Cloudflare Workers MCP 的三层配合这个项目的架构可以拆成三层来看每一层各司其职边界清晰。第一层是Slack 接入层。Slack 提供了 Events API当频道里有新消息、有人加入频道、有表情回应等事件发生时Slack 会向预先配置的 Request URL 发送 HTTP POST 请求。这个 URL 就指向部署在 Cloudflare Workers 上的服务。Slack 还提供了 Slash Commands 和 Interactivity让用户可以通过斜杠命令或按钮与 AI 交互。签名验证Signing Secret是这一层的安全关键必须校验请求确实来自 Slack否则任何人都能伪造请求触发 AI。第二层是Cloudflare Workers 处理层。Worker 收到 Slack 事件后需要做几件事验证签名、解析事件类型、判断是否需要 AI 介入、调用 AI 模型、执行 MCP 工具、把结果返回给 Slack。Cloudflare Workers 的优势在于冷启动极快通常几毫秒、全球边缘节点部署、与 Cloudflare 的 KV、D1、R2 等存储服务无缝集成。对于 Slack Bot 这种“请求来了就处理处理完就结束”的场景Workers 比传统服务器合适得多。第三层是MCP 工具层。MCP 是 Anthropic 提出的开放协议目的是让 AI 模型能以标准化方式调用外部工具和数据源。在这个项目里MCP Server 可以暴露多种工具比如search_conversations用来搜索历史消息、create_task用来在项目管理工具里建任务、query_database用来查内部数据。AI 模型根据用户消息和上下文决定调用哪个工具、传什么参数MCP Client 负责执行调用并把结果返回给模型。这样 AI 就不只是“会说”而是“会做”。三层之间的数据流是这样的Slack 事件 → Worker 验证并解析 → 判断意图 → 调用 AI 模型带 MCP 工具列表→ 模型决定调用工具 → Worker 执行 MCP 调用 → 结果回传模型 → 模型生成最终回复 → Worker 调 Slack API 发消息。整个链路在几百毫秒到几秒内完成用户感知到的就是“AI 在频道里回了一句话而且这句话是基于真实数据说的”。1.3 方案选型背后的取舍为什么不是直接写个 Slack App有人可能会问Slack 本身就有 App 开发框架为什么还要套一层 Cloudflare Workers 和 MCP直接写个 Node.js 服务部署在 VPS 上不行吗行但有几个现实问题。第一Slack App 的传统部署方式需要你维护一个公网可访问的服务器处理 HTTPS 证书、负载均衡、进程守护这些运维成本对小团队来说不低。Cloudflare Workers 把这些全包了你只需要写业务逻辑。第二MCP 的引入是为了解耦。如果直接把所有工具调用逻辑写死在 Slack App 里以后想换 AI 模型、想加新工具、想让同一个 AI 能力同时服务 Slack 和别的平台就得大改。MCP 把“工具定义”和“工具调用”标准化了AI 模型换一个只要它支持 MCP工具层不用动。第三Cloudflare Workers 的免费额度对中小团队足够用每天 10 万次请求基本覆盖一个几十人团队的日常消息量。当然这个方案也有代价。Cloudflare Workers 的运行环境是 V8 Isolate不是完整的 Node.js有些 npm 包不能用需要找替代方案或自己实现。MCP 协议本身还在演进中不同 AI 提供商的实现细节有差异调试起来比直接调 REST API 麻烦一些。但总体来看对于“在 Slack 里跑一个主动干活的 AI”这个目标这套组合是目前比较平衡的选择。2. 核心细节解析与实操要点2.1 Slack App 配置从创建到权限申请第一步是在 Slack API 后台创建一个新的 App。进入 api.slack.com/apps点“Create New App”选择“From scratch”给 App 起个名字比如 company-brain然后选择要安装到的 Workspace。创建完成后你会拿到几个关键凭证Client ID、Client Secret、Signing Secret、Verification Token。其中 Signing Secret 是用来验证请求来源的后面在 Worker 里会用到。接下来配置权限。在“OAuth Permissions”页面需要添加 Bot Token Scopes。根据项目需求至少需要这些app_mentions:read读取 机器人的消息channels:history读取公开频道的历史消息channels:read获取频道列表chat:write以机器人身份发消息groups:history读取私有频道消息如果需要在私有频道工作im:history读取私信消息users:read获取用户信息添加完权限后点“Install to Workspace”授权后会拿到 Bot User OAuth Token格式是xoxb-开头。这个 Token 是 Worker 调用 Slack API 时用的必须保管好不要提交到公开仓库。然后配置 Event Subscriptions。打开“Event Subscriptions”启用 Events在 Request URL 里填入你的 Cloudflare Worker 地址比如https://company-brain.your-subdomain.workers.dev/slack/events。Slack 会立即发送一个 challenge 请求来验证 URLWorker 需要正确响应这个 challenge。订阅的 Bot Events 至少包括app_mention有人 机器人时触发message.channels公开频道有新消息时触发message.groups私有频道有新消息时触发可选注意message.channels事件会收到频道里所有消息包括机器人自己发的。需要在代码里过滤掉bot_id存在的消息否则机器人会自己回复自己形成死循环。2.2 Cloudflare Worker 初始化与签名验证在 Cloudflare Dashboard 里创建一个新的 Worker或者用 Wrangler CLI 本地初始化。推荐用 Wrangler方便本地调试和部署。安装 Wrangler 后运行wrangler init company-brain选择 TypeScript 模板。项目结构大概是company-brain/ ├── src/ │ └── index.ts ├── wrangler.toml ├── package.json └── tsconfig.json在wrangler.toml里配置环境变量和 KV 命名空间用来存 Slack 安装信息、对话上下文等name company-brain main src/index.ts compatibility_date 2024-01-01 [vars] SLACK_SIGNING_SECRET your-signing-secret [[kv_namespaces]] binding COMPANY_BRAIN_KV id your-kv-namespace-id签名验证是 Worker 收到 Slack 请求后的第一道关卡。Slack 会在请求头里带X-Slack-Signature和X-Slack-Request-Timestamp。验证逻辑是把v0:{timestamp}:{raw_body}用 Signing Secret 做 HMAC-SHA256然后和签名比对。时间戳还要检查是否在 5 分钟内防止重放攻击。async function verifySlackRequest( signingSecret: string, timestamp: string, signature: string, body: string ): Promiseboolean { const now Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) 300) { return false; } const baseString v0:${timestamp}:${body}; const encoder new TextEncoder(); const key await crypto.subtle.importKey( raw, encoder.encode(signingSecret), { name: HMAC, hash: SHA-256 }, false, [sign] ); const sig await crypto.subtle.sign(HMAC, key, encoder.encode(baseString)); const hexSig v0 Array.from(new Uint8Array(sig)) .map(b b.toString(16).padStart(2, 0)) .join(); return hexSig signature; }实操心得Cloudflare Workers 的crypto.subtle是 Web Crypto API和 Node.js 的crypto模块用法不同。如果你从 Node.js 项目迁移过来这部分需要重写。另外读取 raw body 时要用await request.text()不能用request.json()因为 JSON 解析会改变 body 的格式导致签名验证失败。2.3 MCP 工具定义与 AI 模型对接MCP 的核心是工具定义。一个 MCP Server 需要暴露一组工具每个工具包含名称、描述、参数 schema。AI 模型根据这些信息决定是否调用、怎么调用。在 company-brain 里可以定义这些工具工具名称功能描述关键参数search_messages搜索 Slack 历史消息query,channel,limitcreate_task在项目管理工具创建任务title,assignee,due_datequery_metrics查询业务指标数据库metric_name,date_rangesummarize_thread总结某个消息串channel,thread_tsschedule_reminder设置提醒text,time,channel工具定义用 JSON Schema 描述参数比如search_messages{ name: search_messages, description: 在 Slack 频道中搜索包含指定关键词的历史消息, inputSchema: { type: object, properties: { query: { type: string, description: 搜索关键词 }, channel: { type: string, description: 频道 ID不填则搜索所有频道 }, limit: { type: number, description: 返回结果数量默认 10 } }, required: [query] } }AI 模型对接方面可以选择支持 MCP 的模型服务。调用时把工具列表和用户消息一起传给模型模型返回的响应里可能包含tool_use块指示要调用哪个工具、传什么参数。Worker 解析这个块执行对应的 MCP 调用把结果再传回模型模型生成最终的自然语言回复。async function callAIWithTools( userMessage: string, tools: any[], context: string ): Promiseany { const response await fetch(https://api.anthropic.com/v1/messages, { method: POST, headers: { Content-Type: application/json, x-api-key: env.ANTHROPIC_API_KEY, anthropic-version: 2023-06-01 }, body: JSON.stringify({ model: claude-3-sonnet-20240229, max_tokens: 1024, tools: tools, messages: [ { role: user, content: ${context}\n\n用户消息${userMessage} } ] }) }); return response.json(); }注意MCP 工具调用可能涉及多轮往返。模型第一次返回tool_use你执行工具后要把结果以tool_result的形式传回去模型再生成最终回复。这个循环最多跑 3-5 轮就要设上限防止无限循环消耗 token。3. 实操过程与核心环节实现3.1 从零部署一个可用的 Slack AI 代理假设你已经有了 Slack App 的凭证和 Cloudflare 账号下面是完整的部署流程。第一步初始化项目并安装依赖。npm create cloudflarelatest company-brain -- --templatetypescript cd company-brain npm install slack/web-api modelcontextprotocol/sdkslack/web-api用来调 Slack API 发消息、查历史。modelcontextprotocol/sdk是 MCP 的官方 SDK用来定义工具和处理调用。第二步编写 Worker 入口逻辑。Worker 的fetch处理函数需要根据请求路径分发/slack/events处理 Slack 事件/slack/commands处理斜杠命令/mcp处理 MCP 调用如果 MCP Server 也部署在同一个 Worker 里。export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): PromiseResponse { const url new URL(request.url); if (url.pathname /slack/events request.method POST) { return handleSlackEvent(request, env, ctx); } if (url.pathname /slack/commands request.method POST) { return handleSlashCommand(request, env, ctx); } return new Response(Not Found, { status: 404 }); } };第三步处理 Slack URL 验证 challenge。Slack 在配置 Event Subscriptions 时会发一个type: url_verification的请求Worker 需要返回challenge值async function handleSlackEvent(request: Request, env: Env, ctx: ExecutionContext) { const body await request.text(); const timestamp request.headers.get(X-Slack-Request-Timestamp) || ; const signature request.headers.get(X-Slack-Signature) || ; const isValid await verifySlackRequest(env.SLACK_SIGNING_SECRET, timestamp, signature, body); if (!isValid) { return new Response(Invalid signature, { status: 401 }); } const payload JSON.parse(body); if (payload.type url_verification) { return new Response(JSON.stringify({ challenge: payload.challenge }), { headers: { Content-Type: application/json } }); } // 处理事件 if (payload.type event_callback) { ctx.waitUntil(processEvent(payload.event, env)); } return new Response(OK, { status: 200 }); }关键点Slack 要求 3 秒内响应否则会重试。所以收到事件后不要同步处理 AI 调用可能耗时几秒而是用ctx.waitUntil异步处理立即返回 200。这样 Slack 不会重试AI 处理在后台完成后再调 Slack API 发消息。第四步实现事件处理与 AI 调用。processEvent函数根据事件类型决定是否触发 AI。对于app_mention事件直接触发对于message.channels事件需要判断消息内容是否包含需要 AI 介入的信号比如问题句、任务分配、决策讨论等。async function processEvent(event: any, env: Env) { if (event.bot_id) return; // 忽略机器人自己的消息 const shouldRespond await shouldAIStepIn(event, env); if (!shouldRespond) return; const context await buildContext(event, env); const aiResponse await callAIWithTools(event.text, MCP_TOOLS, context); const finalReply await handleToolCalls(aiResponse, env); await sendSlackMessage(event.channel, finalReply, event.thread_ts, env); }第五步部署并配置 Slack Request URL。npx wrangler deploy部署成功后会得到一个https://company-brain.xxx.workers.dev的地址。回到 Slack App 配置页面把 Event Subscriptions 的 Request URL 设为https://company-brain.xxx.workers.dev/slack/eventsSlack 会立即验证。验证通过后在频道里 一下机器人应该就能看到回复了。3.2 让 AI“主动干活”的判断逻辑设计“主动干活”是这个项目的灵魂也是最难调的部分。如果 AI 对每条消息都插嘴团队会烦如果太被动又失去了“主动”的意义。我的做法是设计一个轻量的意图判断层用规则 小模型结合的方式决定是否介入。规则层面这几类消息优先触发 AI消息里包含?或且长度超过 20 字可能是提问消息里包含company-brain或brain明确呼叫消息里包含帮我、谁能、怎么、为什么等求助信号词消息里包含决定、结论、同步一下等决策信号词消息串thread超过 5 条回复且没有结论AI 可以主动总结小模型层面可以用一个轻量的文本分类模型比如在 Worker 里跑一个小的 ONNX 模型或者调一个便宜的 API对消息做二分类需要 AI 介入 / 不需要。这个分类器的训练数据可以来自团队历史消息的人工标注初期样本少的时候先用规则兜底。async function shouldAIStepIn(event: any, env: Env): Promiseboolean { const text event.text || ; // 明确呼叫直接触发 if (text.includes(U_BOT_ID)) return true; // 问句信号 if (/[?]/.test(text) text.length 20) return true; // 求助信号词 const helpSignals [帮我, 谁能, 怎么, 为什么, 有没有人知道]; if (helpSignals.some(s text.includes(s))) return true; // 决策信号词 const decisionSignals [决定, 结论, 同步一下, 最终方案]; if (decisionSignals.some(s text.includes(s))) return true; // 消息串过长且无结论 if (event.thread_ts await isThreadTooLongWithoutConclusion(event, env)) { return true; } return false; }实操心得初期不要追求“全自动主动”先做“半主动”——AI 在检测到信号后先在 thread 里发一条“我注意到这个问题需要我帮忙查一下吗”让用户确认后再执行。这样既展示了主动性又不会因为误判打扰团队。等规则调准了再逐步放开自动执行。3.3 MCP 工具调用的完整链路与参数传递MCP 工具调用的链路比普通 API 调用多一层“模型决策”。完整流程是这样的用户消息进入 WorkerWorker 构造 AI 请求带上工具列表AI 模型返回响应可能包含tool_use块Worker 解析tool_use提取工具名和参数Worker 执行对应的 MCP 工具可能是本地函数也可能是远程 MCP Server工具返回结果Worker 把结果以tool_result格式传回 AIAI 生成最终回复Worker 调 Slack API 发消息参数传递的关键是 JSON Schema 的校验。模型生成的参数不一定完全符合 schema比如该传数字的传了字符串该传数组的传了单个值。Worker 在执行工具前要做一次参数规范化function normalizeToolInput(toolName: string, input: any): any { const schema MCP_TOOLS.find(t t.name toolName)?.inputSchema; if (!schema) return input; const normalized { ...input }; for (const [key, prop] of Object.entries(schema.properties || {})) { const p prop as any; if (p.type number typeof normalized[key] string) { normalized[key] parseFloat(normalized[key]); } if (p.type array !Array.isArray(normalized[key])) { normalized[key] [normalized[key]]; } } return normalized; }工具执行本身可以是本地函数也可以是远程 MCP Server。本地函数适合简单查询比如搜索 Slack 消息async function executeTool(toolName: string, input: any, env: Env): Promiseany { const normalized normalizeToolInput(toolName, input); switch (toolName) { case search_messages: return await searchSlackMessages(normalized.query, normalized.channel, normalized.limit, env); case create_task: return await createTask(normalized.title, normalized.assignee, normalized.due_date, env); case query_metrics: return await queryMetrics(normalized.metric_name, normalized.date_range, env); default: throw new Error(Unknown tool: ${toolName}); } }远程 MCP Server 则通过 HTTP 或 SSE 通信Worker 作为 MCP Client 发送请求。这种方式适合工具逻辑复杂、需要独立部署的场景。4. 常见问题与排查技巧实录4.1 Slack 事件收不到或重复收到怎么办这是部署 Slack Bot 时最高频的问题。收不到事件先检查三个地方Request URL 是否配置正确且验证通过、Bot 是否已加入目标频道、Event Subscriptions 是否订阅了对应事件类型。如果 URL 验证失败Slack 后台会显示红色警告点进去看具体错误。常见原因是 Worker 没有正确响应 challenge或者签名验证失败导致 Slack 认为 URL 不可信。重复收到事件通常是因为 Worker 响应超时。Slack 要求 3 秒内返回 200如果 Worker 同步处理 AI 调用导致超时Slack 会重试于是同一条消息触发多次 AI 回复。解决办法就是用ctx.waitUntil把耗时逻辑放到后台立即返回 200。问题现象可能原因排查方法URL 验证失败Worker 未返回 challenge检查url_verification分支收不到消息事件未订阅message.channels检查 Event Subscriptions 配置重复回复Worker 响应超时用ctx.waitUntil异步处理签名验证失败raw body 被修改用request.text()而非request.json()Bot 不回复Bot 未加入频道在频道里/invite company-brain避坑技巧在 Worker 里加一个/health端点返回当前时间戳和版本号。Slack 配置出问题时先 curl 一下这个端点确认 Worker 本身是活的。另外Wrangler 的wrangler tail命令可以实时看 Worker 日志调试时非常有用。4.2 AI 回复质量不稳定怎么调AI 回复质量取决于三个因素上下文是否充分、工具返回的数据是否准确、提示词是否清晰。上下文方面不要只把当前消息传给 AI要把 thread 里的历史消息、频道最近几条消息、相关用户信息都带上。但也不能无限塞要控制 token 数量一般保留最近 10-20 条消息即可。工具返回的数据要结构化。比如search_messages返回的结果不要直接扔一堆原始 JSON而是格式化成易读的列表function formatSearchResults(messages: any[]): string { return messages.map((m, i) ${i 1}. [${new Date(parseFloat(m.ts) * 1000).toLocaleString()}] ${m.user}: ${m.text} ).join(\n); }提示词方面给 AI 明确的角色设定和输出格式要求。比如“你是 company-brain一个团队协作助手。回答要简洁直接给结论不要客套。如果需要调用工具先调用工具再回答。如果信息不足明确说需要什么信息。”4.3 Cloudflare Workers 的限制与绕行方案Workers 有几个硬限制需要提前知道CPU 时间默认 10ms付费版 50ms、单次请求内存 128MB、不支持 Node.js 原生模块。对于 AI 调用这种场景CPU 时间主要花在 JSON 解析和签名验证上10ms 通常够用。但如果要在 Worker 里跑本地模型推理就不够了得用远程 API。不支持 Node.js 原生模块意味着fs、path、cryptoNode 版都不能用。Web Crypto API 可以替代大部分加密需求。如果依赖的 npm 包用了 Node API可以找浏览器兼容版本或者用 Cloudflare 的nodejs_compat标志开启部分兼容。compatibility_flags [nodejs_compat]实操心得Workers 的 KV 存储有最终一致性写入后立刻读可能读到旧值。如果需要强一致用 D1 数据库或 Durable Objects。对于 Slack Bot 的对话上下文KV 够用因为上下文稍微旧一点不影响大局。但如果是任务状态这种不能出错的数据一定要用 D1。4.4 MCP 工具调用失败的排查清单MCP 调用失败通常表现为 AI 回复“我尝试调用工具但失败了”或者直接报错。排查顺序如下检查工具定义是否在请求里正确传递。打印 AI 请求的tools字段确认 schema 格式正确。检查模型返回的tool_use块是否被正确解析。有时候模型返回的 JSON 格式和预期有差异需要做兼容处理。检查工具执行函数是否抛异常。在executeTool里加 try-catch把错误信息返回给 AI让 AI 决定怎么回复用户。检查tool_result是否被正确传回模型。格式必须是{ type: tool_result, tool_use_id: ..., content: ... }。检查多轮循环是否超限。设一个最大轮数比如 5 轮超过就返回“处理超时请稍后重试”。async function handleToolCalls(aiResponse: any, env: Env, maxRounds 5): Promisestring { let rounds 0; let currentResponse aiResponse; while (rounds maxRounds) { const toolUse currentResponse.content?.find((c: any) c.type tool_use); if (!toolUse) break; let toolResult; try { toolResult await executeTool(toolUse.name, toolUse.input, env); } catch (e) { toolResult { error: 工具执行失败: ${e.message} }; } currentResponse await callAIWithToolResult(toolUse.id, toolResult, env); rounds; } const textBlock currentResponse.content?.find((c: any) c.type text); return textBlock?.text || 处理完成但没有生成回复。; }5. 扩展方向与个人实践体会5.1 从单频道到多频道权限与上下文隔离初期建议只在一个频道里跑比如#general或#engineering。跑稳之后再扩展到多频道。多频道带来的第一个问题是权限AI 在私有频道里能不能读消息、能不能发消息取决于 Bot 是否被邀请进频道。第二个问题是上下文隔离不同频道的对话上下文不能混在一起否则 AI 会把 A 频道的讨论带到 B 频道。我的做法是在 KV 里按频道 ID 存储上下文key 格式是context:{channel_id}:{thread_ts}。每次处理消息时只读取当前频道当前 thread 的上下文。跨频道的知识检索通过search_messages工具显式触发而不是隐式混入上下文。5.2 接入更多 MCP 工具从 Slack 到全工作流company-brain 的价值不只在于 Slack 内部。通过 MCP它可以接入团队用的所有工具Jira、Notion、GitHub、Figma、Google Calendar。比如有人在 Slack 里说“这个 bug 谁跟一下”AI 可以自动在 Jira 建 issue、分配给最近提交过相关代码的人、把链接发回 Slack。又比如有人说“下周的评审会准备一下”AI 可以查日历找时间、在 Notion 建会议纪要模板、把待办同步到任务列表。每接入一个新工具只需要定义一个新的 MCP 工具 schema 和对应的执行函数。AI 模型会自动学会什么时候调用它。这种扩展方式比传统“写死工作流”灵活得多因为决策权在模型手里你只需要把工具准备好。5.3 成本控制与性能优化AI 调用是主要成本。以 Claude 3 Sonnet 为例输入 1000 token 约 0.003 美元输出 1000 token 约 0.015 美元。如果每条消息都触发 AI一个活跃频道一天可能产生几百次调用成本会上去。优化手段有几个用规则过滤掉明显不需要 AI 的消息、对简单问题用更便宜的模型比如 Haiku、缓存常见问题的回答、限制上下文长度。性能方面Worker 到 AI API 的网络延迟是大头。如果 AI 服务在美东Worker 也在美东延迟可以控制在 200-500ms。如果跨区域可能到 1-2 秒。Cloudflare Workers 可以配置智能路由但 AI API 的端点通常固定优化空间有限。能做的是在 Worker 里并行处理签名验证、上下文读取、工具预查询可以并行减少串行等待。我个人在实际操作中的体会是这个项目最值得投入时间的地方不是 AI 模型本身而是“判断逻辑”和“工具设计”。模型能力已经足够强真正决定体验的是AI 该在什么时候出现、出现后能做什么、做完后怎么把结果清晰地告诉团队。把这三件事打磨好哪怕用中等能力的模型效果也比用最强模型但乱插嘴好得多。另外初期一定要加一个“静默模式”开关团队觉得吵的时候能一键关掉不然很容易被嫌弃然后整个项目被砍掉。