这是一个运行在你自己的 Cloudflare(Cloudflare)账户里的只读服务器,用来让 AI 代理读取 WhatsApp(WhatsApp)消息。你只需像关联 WhatsApp Web(WhatsApp Web)那样把手机连接一次,之后任何支持模型上下文协议(MCP)的客户端——无论是克劳德代码(Claude Code)、克劳德桌面端(Claude Desktop)还是其他兼容工具——都能搜索并阅读你的聊天记录;它能读取、搜索消息,并记住你为手机号起的名字,但不能发送任何内容。
正文
让你的 AI 助手读取你的 WhatsApp 消息。
这是一个运行在你自己 Cloudflare 账号里的小型服务器。你只需把手机像关联 WhatsApp Web 那样关联一次,之后任何 MCP 客户端(Claude Code、Claude Desktop,或任何支持 MCP 的客户端)都能搜索并读取你的聊天记录。它可以读取和搜索消息,也能记住你给电话号码起的名字,但不能发送任何内容。
你可以让助手做这些事:
- “Dana 关于航班说了什么?”
- “在我所有聊天里,找出每条提到租约的消息。”
- “总结一下这周家庭群里我错过了什么。”
工作原理
手机始终是消息的真正来源,服务器只是助手与之通信的一个已关联设备。
它并不会一直连着 WhatsApp。当助手发起请求时,以及每天一次,它才会连上 WhatsApp,收取自上次以来 WhatsApp 为它排队的消息,写进自己的数据库,然后断开。手机照常工作,也会继续收到通知。
你的消息存放在你 Cloudflare 账号里一个 Durable Object 内的 SQLite 数据库中,不会流向其他地方。
安装设置
你需要一个开通 Workers Paid 套餐的 Cloudflare 账号(见下面说明)、Node 20 或更高版本,以及你的手机。
- 克隆仓库并安装依赖:
git clone https://github.com/finereli/whatsapp-mcp,cd whatsapp-mcp,npm install。 - 创建登录 token 的存储位置:
npx wrangler kv namespace create OAUTH_KV,把打印出的 id 粘贴到wrangler.jsonc。 - 设置关联手机页面的密码:
npx wrangler secret put OWNER_PASSWORD,至少 12 个字符。 - 部署:
npm run deploy。
Wrangler 会打印出你的 URL,类似 https://whatsapp-mcp.your-name.workers.dev。打开它,首页还有两步:
- 关联 WhatsApp。 用 WhatsApp 扫描二维码(设置 → 关联设备 → 关联设备)。如果你就在手机上、无法扫描屏幕,就改用电话号码选项。首次关联会导入历史记录,页面会显示进度,之后就可以关闭。
- 连接助手。 在 Claude Code 中执行:
claude mcp add --transport http whatsapp https://your-url/mcp,然后输入/mcp并用密码登录。其他客户端把地址指向同一个/mcpURL 即可。
如果想用自己的域名,见 wrangler.jsonc 底部的注释。
Workers 付费说明
导入大量历史记录会一次性解码很多数据,而 Workers 默认的 CPU 限制太小。wrangler.jsonc 调高了这个限制,但调高限制需要付费套餐,每月几美元。我没试过免费套餐。如果你用的是免费套餐,就删掉 limits 块,并且要预期首次导入可能不稳定。
使用前须知
这不是官方功能。 WhatsApp 没有提供读取个人消息的 API。本项目通过开源库 Baileys 使用与 WhatsApp Web 相同的协议,这违反了 WhatsApp 的条款。对于一个只读取的关联设备,实际风险很低,但不是零。请在自己的账号上使用,也不要用它发消息。
你只能拿到 WhatsApp 给出的内容。 首次关联会导入 WhatsApp 决定交给新设备的历史记录,可能不是全部,而且每次关联只做一次。想再次导入就要重新关联(已存消息会保留,重新导入会补上缺失的部分)。
有些聊天只显示电话号码。 WhatsApp 不会把全部联系人名给关联设备,这些大多是没存过的陌生人发来的一次性消息。对于重要的号码,助手可以修正:列出未命名聊天,用它已有的联系人工具(比如 Google Contacts 或 Gmail 连接器)查找号码并保存名字。服务器本身从不连接联系人服务。参见下面的“命名聊天”。
媒体文件不会下载。 照片、语音消息和文档会显示为 [image] 或 [voice note 12s] 这样的标签,有字幕的话会带上,但不会做任何转写。
它只服务一个人。 一次部署对应一个 WhatsApp 账号和一个所有者。这是有意设计的:每次部署都持有完整消息历史和一个实时登录态,所以每个人都应该自己部署自己的实例。
重新关联。 如果手机提示设备已被移除,或你想重来,可以让助手运行 relink,或打开 /setup 选择“Unlink and scan again”。然后扫描新二维码,已存消息会保留。
助手能做什么
| 工具 | 作用 |
| --- | --- |
| list_chats | 按最近顺序列出聊天,可按名字或号码过滤。 |
| get_messages | 读取某个聊天,可按日期向前/向后翻页。 |
| search_messages | 在全部或单个聊天内全文搜索。 |
| unnamed_chats | 只知号码的聊天,按最活跃排序,并附上对方最后一条消息。 |
| set_contact_names | 为电话号码保存名字(格式不限)。名字绑定在号码上,因此也会出现在群聊中。 |
| sync_now | 立刻拉取新消息。 |
| link_status | 查看是否已关联、上次拉取时间以及存储量。 |
| relink | 解除关联并重新开始,需要人工扫描新二维码。 |
每个读取工具都会先检查:如果本地存储的消息已超过十分钟,就先从 WhatsApp 补齐。
命名聊天
WhatsApp 只会把服务器知道的名字给它,其余的都显示为电话号码。与其让服务器登录联系人服务(那意味着一个云项目、同意屏幕,用 Google 的话还要应用审核),不如把这项工作交给助手——助手通常已经有办法访问你的联系人。
对它说“给我的未命名聊天找名字”。助手会调用 unnamed_chats,把号码和你的联系人比对,再用 set_contact_names 保存找到的名字。它不可能找到所有人:大多数未命名聊天只是某个陌生人发来的一条消息。
助手会自动被引导去做这件事:服务器的指令里提到了它,每次出现“no chat found”或“no matches”的结果时也会提示,因为那正是助手卡在某个名字上的时刻。link_status 也会报告有多少聊天未命名。
面向改代码的人和 AI 助手
以下内容面向要改动代码的人,包括基于 fork 工作的 AI 助手。文件开头部分则是给部署实例所有者看的。
整体结构
MCP 客户端通过 OAuth 连到 Worker(src/index.ts)。Worker 的 /mcp 路由交给 mcp.ts,每个请求新建一个 McpServer;/、/setup、/authorize、/login/*、/pair/* 提供落地页、登录和配对界面;cron 任务在 UTC 04:00 触发 pull()。pull() 通过 RPC 调用名为 owner 的 Durable Object(WhatsAppStore)。该对象的 SQLite 保存认证密钥、消息、聊天、联系人和 FTS5 索引,并通过仅在配对或拉取会话期间打开的 Baileys socket,经 WebSocket 连接 WhatsApp 服务器。所有状态都集中在这一个 Durable Object 里,它的 SQLite 同时充当 Baileys 的认证存储和消息存储。Worker 本身没有状态,只保留一个供 OAuth provider 存放 token 的 KV namespace,以及一个短期的“待登录”记录。
文件说明
| 文件 | 职责 |
| --- | --- |
| src/index.ts | 路由:OAuth provider 接线、登录、配对端点、/debug、cron。 |
| src/auth.ts | 判定谁是所有者。两种模式:密码或登录服务,以及所有者的浏览器会话。 |
| src/phone.ts | 把任意格式的电话号码转成 WhatsApp jid。 |
| src/whatsapp-do.ts | Durable Object:运行 Baileys 会话,向 Worker 暴露 RPC 方法。 |
| src/store.ts | 数据库 schema、把 Baileys 事件写入 SQLite,以及所有查询。 |
| src/auth-state.ts | 由 DO 的 SQLite 支撑的 Baileys 认证状态(替代 useMultiFileAuthState)。 |
| src/mcp.ts | MCP 工具,以及把助手引向命名工具的提示。 |
| src/pages.ts、src/landing.ts、src/privacy.ts、src/tos.ts、src/ui.ts | 全部 HTML。ui.ts 含共享设计 token。 |
| src/ws-shim.ts、src/lru-shim.ts、src/polyfills.ts | 让 Baileys 能在 workerd 上运行,见下文。 |
| scripts/vendor-bridge.mjs | 从 Baileys 的 Rust bridge 中提取 wasm,在 npm install 时运行。 |
| local-export/ | 一个独立的一次性脚本,无需服务器即可把历史记录导出为 JSON 文件,不属于部署部分。 |
会话如何工作
所有 WhatsApp 通信都发生在 whatsapp-do.ts 的 runSession() 里,分两种:
- 配对(
startPairing)。此时还没有凭证。Baileys 发出二维码字符串,保存在内存中,由pairStatus()渲染成 SVG。扫描后,WhatsApp 会以代码 515(需要重启)关闭 socket,会话随新凭证重连。历史记录以messaging-history.set分片到达,并带progress百分比。当 WhatsApp 报告 100% 且数据流静默几秒后,会话结束。 - 拉取(
pull)。用已存凭证重连。WhatsApp 先下发离开期间排队的消息,然后发出receivedPendingNotifications。静默几秒后会话结束。
调用方共享同一个会话:在一个 pull() 运行期间再调用 pull(),会得到同一个 promise。
配对会话比发起它的 HTTP 请求活得更久。startPairing 会设置一个 Durable Object alarm,alarm() 等待会话 promise,正是这个在飞行中的 alarm handler 让对象保持存活。配对页面只是在旁边观察,关掉它并不会中断导入。
空闲检测只统计数据事件(messaging-history.set、messages.upsert),不算连接层的杂务。如果把什么都算进去,会话永远不会结束,因为在线状态和联系人更新不会停。
数据模型
全部存放在 Durable Object 的 SQLite 中(store.ts、initSchema):
messages(chat, id, ts, from_me, sender, push_name, type, body),主键为(chat, id)。媒体在body里只是一个标签;编辑会更新body,删除则把它设为[deleted]。messages_fts:基于body的 FTS5 索引,由触发器保持同步。chats(jid, name, is_group)、contacts(jid, name, notify)和custom_names(jid, name)。显示名的解析顺序是:WhatsApp 的聊天名 → WhatsApp 的联系人名 → 已保存的名字 → 对方的 push name → 号码。set_contact_names写入的就是custom_names。alias(lid, pn):WhatsApp 正在迁移到匿名@lid标识符。这张表把它们映射到电话号码 JID,addAlias会把已有行合并成电话号码形式,确保一个人对应一个聊天。auth(k, v):Baileys 凭证和 signal 密钥,按 Baileys 的BufferJSON编码的 JSON。meta(k, v):目前只有last_pull。
写入是幂等的。重新导入历史记录时会执行 upsert:补上缺失的 sender 或 push_name,且不会产生重复。这正是重新关联安全的原因。
改代码前要知道的事
下面每一条都是花了很多时间才发现的。改动 Baileys 相关部分前请先读完。
- Baileys 需要三个 shim 才能跑在 workerd 上。
ws:该包需要 Node socket。ws-shim.ts包装 Workers 的 WebSocket,用fetch()和Upgrade头打开,因为这是唯一能设置 WhatsApp 会检查的Origin头的方式。whatsapp-rust-bridge:它以 base64 形式发布 wasm 并在运行时编译,而 workerd 不允许。vendor-bridge.mjs把 wasm 写成文件,并修改加载器,使其作为CompiledWasm模块导入。生成的vendor/目录不提交。- 定时器:Node 定时器有
.unref(),workerd 的没有。polyfills.ts补上了这个方法,且它必须是 Worker 和 Durable Object 模块里的第一个 import。
lru-cache的自动清理会破坏首次导入。Baileys 创建缓存时用了ttlAutopurge: true,这会为每条记录启动一个setTimeout。workerd 每个对象只允许 10,000 个待处理定时器,首次历史同步会瞬间超出并抛QuotaExceededError。lru-shim.ts关掉了自动清理;过期条目仍在访问时惰性丢弃。- 二维码配对永远不会设置
creds.registered。那个标志属于手机注册流程。判断“是否已关联”要看creds.me?.id。 - 每次关联只来一次历史记录。如果首次导入中途失败,那段历史就没了,只能重新关联。改动导入代码后,一定要用真实的首次关联测过再发布。
- 群聊发送者出现在两个地方。实时消息在
key.participant,历史消息则在msg.participant。两处都要读。 - 精确锁定 Baileys 版本。它还处于预发布阶段,API 在不同 release candidate 之间会变。
- 部署不会驱逐忙碌的 Durable Object。带有活动会话的对象会一直跑旧代码,直到会话结束。即使用 migration 重命名类也没能驱逐它。如果你发布了修复但对象仍然异常,就停止向它发请求,等三到五分钟。每次探测(包括
/debug)都会重置它的空闲计时器,所以轮询一个陈旧对象只会让它一直陈旧。Baileys 在 socket 关闭时还会泄漏 interval 定时器;whatsapp-do.ts里的finish()会清掉它们,这样对象才有可能进入空闲。 - 登出分两步。
logout()会先连接并调用 Baileys 的logout(),让设备从手机列表里消失,然后清除本地凭证。如果第一步失败,它会写在 debug 事件里,用户得手动移除设备。 - WhatsApp 在没有名字的地方会存一个占位符。对于没有联系人的聊天,它会用号码命名,有时还会打码(
+972∙∙∙∙∙∙∙20),聊天名和联系人名里都有。store.ts里的numberLike把这些当作“没有名字”,写入时也拒绝存储。在这个修复之前,占位符会压过真实保存的名字。 - 不要调用
markOnlineOnConnect。它被故意设为 false。把关联设备标记为在线会让手机不再显示通知。
配置项
| 名称 | 类型 | 用途 |
| --- | --- | --- |
| OWNER_PASSWORD | secret | 密码模式(默认)。至少 12 个字符,否则登录会被拒绝。 |
| OWNER_EMAIL | var | 登录服务模式:允许登录的唯一邮箱。 |
| OPERATOR_NAME | var,可选 | 在隐私页面上显示为谁运行此服务器。 |
| DEFAULT_COUNTRY_CODE | var,可选 | 国家代码(纯数字,不带加号)。当助手保存没写国家码的号码时使用,例如 972。 |
| LOGIN_ORIGIN | var | 设置后切换到登录服务模式。 |
| ALLOWED_ORIGINS | var | 登录服务模式:接受其 token 的逗号分隔来源。生产环境应只列生产来源。 |
| PIN | secret,可选 | 32 位以上随机字符。通过 x-pin 头启用 /debug 路由;不设置则关闭。 |
| OAUTH_KV | binding | OAuth provider 和待登录记录的 token 存储。 |
| WA | binding | Durable Object。 |
把某次部署自己的值(域名、KV id、邮箱)放在 gitignore 的 wrangler.local.jsonc 里,并用 npm run deploy:local 部署。wrangler.jsonc 是给别人起步用的模板。
如何改造
- 新增工具。 在
mcp.ts注册,SQL 放在store.ts。读取类工具应先调用fresh()。除非有意如此,否则保持工具只读。如果失败会让助手卡住,就在工具结果里说明下一步该试什么,就像NAME_HINT对缺失名字所做的那样。 - 改登录方式。
auth.ts选择模式(authMode),index.ts的路由随之变化。要接入新的身份 provider,就在那里加一个模式和一个回调路由。其余代码只看到一个不透明的 bearer token。 - 改成多用户。 目前 Durable Object 用固定名字
owner寻址(index.ts的wa()、mcp.ts的handleMcp),OAuth props 里的userId也是常量'owner'。要多用户,就把两者都从登录身份派生,并按身份给对象做 key。先想清楚你是否愿意持有别人的消息历史。 - 其他补名字的方式。 任何能产出
{ jid, name }行的东西都可以调用store.setCustomNames。CSV 上传、CardDAV 拉取或定时任务都行。走助手这条路,是为了让服务器不需要任何凭据。 - 发送消息。 故意没做。加这个功能前请三思。
调试
设置 PIN 后,以下路由返回 JSON,且需要请求头 x-pin: <PIN>:
/debug:会话阶段、是否已关联、计数,以及会话最近事件(连接变化、历史分片大小、错误)。/debug/pull:立刻执行一次拉取。/debug/chats?q=、/debug/messages?chat=、/debug/search?q=、/debug/types:不经 MCP 直接读存储。/debug/unnamed:unnamed_chats会返回的聊天。/debug/sql?q=:对存储执行一次只读SELECT,用来排查名字或计数为何不对。/debug/logout:和relink相同。
本地开发:npm run dev,并在 .dev.vars 里写上 OWNER_PASSWORD=...(至少 12 个字符)。它会跑一个带有独立空存储的本地 Durable Object,因此你可以走通登录流程并看到真实二维码,但从本地开发关联手机会产生第二个关联设备。
不想用手机测试配对界面状态时,可以用一个模拟的 /pair/status 来提供页面。页面需要的就是那一个 JSON:字段有 paired、running、progress、qrSvg、messages、chats。
验证改动
npm run typecheck,然后npm test。测试会用 Node 自带的 SQLite(Node 22.13 或更高)跑真实的 store 代码,包括从旧版本迁移。那个 SQLite 没有 FTS5,所以搜索本身没被覆盖。- 部署后带 PIN 请求
GET /debug,应返回phase: "idle"和paired: true。 - 请求
GET /debug/pull。安静的账号应在约十秒内返回reason: "idle"。 - 调用
list_chats,然后在希伯来语或大量 emoji 的聊天上调用get_messages,再调用search_messages。接着调用unnamed_chats,并对一个号码执行set_contact_names,确认聊天里显示出名字。 - 如果改了导入逻辑,重新关联并确认已存消息数没有增长(重新导入是幂等的),且群消息显示真实发送者名字。
已知不足
- 在 WhatsApp 里没有名字、也没保存过的聊天,会显示为电话号码。系统不会自己刷新名字:只有助手(或你)主动保存才会更新。
- 如果每日拉取时发现会话已登出,没有任何东西会通知你。下次助手询问时,
link_status会显示出来。 - 编辑和删除是在作为协议消息到达时应用的,这条路径还没用真实消息测过。
- 目前只在一个真实账号上实际跑过。
许可证
MIT
评论
讨论整体偏质疑和警惕,多数人担心把 WhatsApp 对话经 Cloudflare 与第三方 AI 处理会侵犯未同意者的隐私、违反服务条款并导致封号,也有人嘲讽所谓“免费”并不真实;少数人则肯定这是接入个人 AI 助手或 WhatsApp 机器人的有趣尝试,并主张用本地或自建桥接来降低风险。分歧主轴是“个人助手的实用性与便利性”和“隐私、同意与账号安全”之间的冲突。
认为项目会把未同意的聊天对象隐私暴露给第三方 AI 和 Cloudflare
- 消息会经 Cloudflare 与第三方 AI 处理,被读取甚至纳入训练数据;Cloudflare 本就嗅探大量互联网流量,再让其接触私人消息意味着进一步中心化。使用者本人或许不在意,但对话另一端的人从未同意自己的消息被上传,消息中常有机密内容,因此对他人不公平且侵犯隐私。(较多人) > “麻烦不在使用者,而在于 WhatsApp 另一端有许多从未同意的人,这对他们很不公平。”(@fantasticwaddle)
认为使用这类 WhatsApp 机器人违反服务条款,账号几乎必然被封
- 用机器人或非官方库访问 WhatsApp 违反服务条款(ToS),账号可能被停用;即使按官方流程关联多设备,也有人在使用几年后突然被判违规并立即中断访问,需等待 24 小时以上复审,因此借 Cloud 运行这类项目被视为近乎保证永久封禁。在 WhatsApp 是全民通讯渠道的地区,封号代价尤其大。(较多人) > “我仅仅按官方推荐流程关联设备就被突然判为违反条款并封号,用这种方式几乎等于必然被永久封禁。”(@lgleason)
嘲讽所谓“免费”并不真实,且不信任把消息托管在 Cloudflare
- 标题和正文都强调免费,但仓库 README 实际说明 Workers 默认 CPU 限制太小,需要每月几美元的付费计划,原作者也没试过免费方案;有人质疑 Cloudflare 到底贡献了什么,是否只是提供免费托管。对“免费”的怀疑也浓缩在一句玩笑里。(少数人) > “免费的通讯 MCP,能出什么错?”(@neuropacabra)
认为这是接入个人 AI 助手或 WhatsApp 机器人的有趣探索,但应尽量本地或自建部署
- 少数人承认安全顾虑,但认为看不到对话数据的个人助手没有意义,因此项目是有价值的阶梯,可用于 WhatsApp AI 助手、客服机器人或构建个人上下文图谱;他们同时强调应在自有设备或自有桥接上运行,只从本地观察和暴露数据,以避免把对话交给第三方云。(少数人) > “个人助手若看不到对话数据就没用,所以这很棒;但我主张在家庭设备上保持账号登录并从本地观察数据。”(@buremba)
有价值的补充
- 自建替代方案:可用 AI 在极少提示内生成 Android 网关来收发短信,并通过 WireGuard 连接本地 AI,做到零 SaaS、零第三方、有日志且可限制访问范围。(@pizzafeelsright)
- 同类实现可以让 MCP 跑在自己的 WhatsApp Web 或自有 App 里,本地运行或通过开源桥接运行;也有团队做了略有差异的 kaptionai.com/mcp。(@kshmir)
- 现成库并不省心:Baileys 等 whatsapp-web 类库可能需要定期手动重新登录,个人机器人仍缺少友好平台,Telegram 或 Discord 可能更合适;若改用 Twilio 等官方 API,需警惕其 KYC 体验很差。(@jrecyclebin、@jiggunjer)
- 同一本地化思路也被扩展到 LinkedIn、Instagram、X 等:在家庭设备浏览器保持登录并读取时间线来生成摘要,避免无脑刷屏。(@buremba)