实时 Slack 通知
学习目标
到本节课结束,学员应当能够:
- 创建一个 Slack App 并拿到 incoming webhook URL —— 人需要做的最少功——然后把其它一切交给 Cursor。
- 做一个联系表单,让它在一个 server action 里既写 Neon 也调 Slack webhook,全部通过一句端到端描述功能的提示词搞定。
- 把"通知即可观测性"刻进脑子:为什么"每个真实事件都触发一条小小的 Slack ping"是资深工程师 实际 监控小型生产产品的方式。
核心主题
- Slack Apps、Incoming Webhooks,以及为什么这是世界上最便宜的实时管道。
- Server action 作为 webhook 调用的"信任边界"——webhook URL 是 secret。
- Fan-out 模式:一个事件 → 两个目的地(数据库 + Slack)。其中一个失败时怎么决定重试 / 记日志 / 丢弃。
- 反垃圾:rate limit、honeypot 字段、内容过滤——保护你的 Slack 不被一个无聊的 bot 打 400 次。
工具 / 技术栈
| 工具 | 本周角色 |
|---|---|
| Slack | 通知落地的地方 —— 你自己的 workspace。 |
| Slack Incoming Webhooks | 最简单的"HTTP-POST-到-Slack"机制。 |
| Next.js server actions | 表单 handler 所在地;secret 只能 server 端看到。 |
| Drizzle + Neon | 新表 contact_submissions 持久化每一次提交。 |
@upstash/ratelimit(可选) | 滑 动窗口 rate limit。 |
课堂计划
| 时间 | 活动 |
|---|---|
| 0 – 15 分钟 | 回顾与 Check-in。 谁拿到了第一个博客订阅者?有没有意外的回复? |
| 15 – 40 分钟 | 概念讲解。 Slack incoming webhook 在 HTTP 层是怎么工作的(让 Cursor 用 curl 演示一下)。为什么 webhook URL 是 secret。Fan-out 与失败模式。为什么你"第一次的生产监控"应该是一个 Slack 频道。 |
| 40 – 75 分钟 | 现场演示。 讲师在自己的站上加联系表单,看着一位同学提交它——Slack 消息实时弹在投影仪上。全班鼓掌。 |
| 75 – 105 分钟 | 动手 Lab。 学员搭自己的联系表单;每个人给某一位同学的表单提交一条消息,互相测试实时 ping。 |
| 105 – 120 分钟 | Q&A + 收尾。 谁的 Slack ping 没到,当场调——通常是 URL 写错或环境变量没设。 |
动手 Lab
任务。 下课时,你的线上站有 Contact 页,带表单。提交时,消息存到 Neon 并且 实时触发一条 Slack 消息到你 workspace 里 #my-site 频道——2 秒以内到达,附带发件人姓名、邮箱、消息正文。
阶段 1 —— 配 Slack
在浏览器打开 api.slack.com/apps:
- 点 Create New App → From scratch。命名
My Portfolio Contact。选择你想让通知落地的 workspace(用你个人的就行——没有就新建一个)。 - 在 app 设置里点 Incoming Webhooks → 把 Activate Incoming Webhooks 打开。
- 点 Add New Webhook to Workspace。选(或新建)一个频道叫
#my-site-contact。同意。 - 复制生成的 webhook URL(形如
https://hooks.slack.com/services/T.../B.../...)。这个 tab 别关。
Why manual: Slack 的 app 创建和 workspace 安装流程在多个步骤上需要人同意。Slack 明确不暴露任何能替用户创建 app 的 API——是它故意这样设计的。
阶段 2 —— 把 webhook 接进项目
这是我的 Slack incoming webhook URL:[粘贴]。
请:
- 把
SLACK_CONTACT_WEBHOOK_URL通过 Vercel CLI 加到.env.local和 Vercel 的 production + preview 环境。- 装上
zod做输入校验,以及@upstash/ratelimit+@upstash/redis做 rate limit(如果我还没配 Upstash,先跳过 rate limit——下一步用更简单的办法)。- 加一张新的 Drizzle 表
contact_submissions,列:id(serial 主键)、name(text,1–80 字符)、message(text,10–2000 字符)、user_agent(text,可空)、ip_hash(text,可空——我们 hash IP,永不存原文)、created_at(timestamp 默认 now)。- 生成并 push 迁移。
继续之前,确认 webhook URL 看起来是合法的 Slack URL,而不是我可能粘错的别的 URL。(regex 检查:必须以
https://hooks.slack.com/services/开头。)
- Cursor 确认 webhook URL 格式正确。
contact_submissions表存在于 Neon(Cursor 查information_schema证明)。SLACK_CONTACT_WEBHOOK_URL在.env.local和 Vercel 的 prod + preview 都设了。
阶段 3 —— 做联系表单 + server action
做
/contact页和它的 server action:UI(
/contact):
- 干净的表单:Name、Email、Message textarea、Send 按钮。匹配站点 design tokens 和强调色。
- message 字段实时字符计数(10 最少 / 2000 最多)。
- 一个可见的隐藏蜜罐字段叫
website—— label 起一个无聊的名字 Your website (optional),用 CSS 视觉上隐藏(不要用hidden属性),server 端拒绝任何这个字段被填的提交。- 提交后:显示成功状态 "Thanks! I'll be in touch within 48 hours."。不要跳走。
server action(
app/contact/actions.ts):
用 zod 校验输入。蜜罐被填的提交静默拒绝(返回成功 UI 但不持久化、不通知——这能迷惑 bot)。
把请求 IP 用 SHA-256 hash,提取
user-agent;两者都存。INSERT 到
contact_submissions。通过
fetch(POST)触发 Slack webhook,消息形如:New contact form submission From: [name] (
\[email\]) Message: [message, 前 400 字符] received [ISO timestamp]如果 Slack 调用失败,仍然写 DB 行 + 记日志——绝不因为 Slack 抽风而丢用户的消息。
给表单 UI 返回合适的 success / error shape。
- localhost:用合法数据提交表单。出现成功状态。
- ~2 秒内 Slack 消息到达
#my-site-contact。 - Neon 里有这一行。
- 蜜罐被填提交——表单显示成功状态,但 Slack 和 Neon 都没东西到(静默丢)。
- message 长度 5 提交——表单报校验错,不提交。
阶段 4 —— 给 bot 设阻力,不给人设
加两层便宜的反垃圾:
- 按 IP hash rate limit: 每小时 5 次。用一个内存 LRU(带 TTL 的 Map)就行——我们没装 Upstash,课堂用够了。超额时返回 429,显示 "You've sent a lot of messages recently — try again in an hour."
- 内容启发式: message 里超过 3 个 URL,或者 message 和最近 10 分钟内某次提交完全相同,则静默丢(同蜜罐 UX)。
如果我配了第二个 webhook 到
#my-site-spam,每次 drop 给一条小通知;否则只在 server 端记日志。
- 同一个浏览器一连发 6 次 —— 第 6 次 rate-limited。
- 10 分钟内同一条消息提交两次——第二次静默丢。
- 一条 5 个 URL 的消息——静默丢。
阶段 5 —— 上线
全部 commit,push,等 Vercel 部署。线上后,自己从生产 URL 提交一次测试表单,确认 Slack webhook 在 prod 也工作(环境变量有时会被错加到
preview)。再做一件事:在站点 header 里 Blog 旁边加一个小 Contact 链接。
- header 里现在有 Blog · Contact 在已有 nav 旁边。
- 一条生产环境的提交在 2 秒内出现在 Slack 里。
- Neon 行里 IP 是 hashed 的(不是原文)。
20 分钟前 Slack 消息停了,但表单仍然返回成功。请检查 Vercel 上 contact action 的 function 日志,找到 Slack POST 在哪里挂掉,并引导我修。优先级:除非我们明确决定可以这样,否则不要在 没有 Slack 投递的情况下继续存用户消息。
任何拿到你 Slack webhook URL 的人都可以往你频道里发任意内容,包括垃圾或冒犯内容。永远不要 把它 commit 到 GitHub、永远不要在公开截图里露出来、 永远不要在 DM 里 "给你看个链接" 的方式发。一旦泄露,立即在 Slack app 设置里 rotate —— 拿到新 URL 后,Cursor 一句话就能更新你 .env + Vercel env vars。
本周作业
做 / 交付。
- 线上
/contact页,存 Neon 并通知你的 Slack。 - rate limit + 蜜罐 + 重复内容防护都工作。
要求。
- 蜜罐字段在正常 UI 里看不见,但在 DOM 里存在。
- IP 都 hash,不存原文。
- 同学的电脑上提交一条,3 秒内你的 Slack 收到 ping。
- 一张你 Slack 频道截图,至少 3 条来自同学的提交。
提交。 第 8 周开课前,把线上 URL + Slack 截图发到课程频道。
资源
| 文档 | 视频 | 仓库 |
|---|---|---|
| Slack Incoming Webhooks —— 文档 | 讲师 demo:"实时 Slack ping,12 分钟" | vercel/next.js/examples/with-server-actions |
| Slack Block Kit —— 让消息更好看(可选) | ||
zod —— schema 校验 | ||
| Next.js server actions —— docs |