这是 WhatsApp 平台桥接项目,负责在 Electron 主进程中管理 WhatsApp Web 多账号实例,并通过 @wppconnect/wa-js 完成 WhatsApp 消息读取和宿主通知文本发送。
当前阶段提供数据读取,以及宿主层通知到 WhatsApp 文本消息的发送能力:
- 多账号登录和多开。
- 获取联系人列表。
- 获取会话列表。
- 获取当前 WhatsApp 选中会话的名称和消息。
- 获取全部已加载/可读取消息。
- 标准化消息类型、来源、去向、所属账号和所属会话。
- 监听新入站消息,并转成统一主题事件。
- 监听单条消息删除与编辑事件,并转成统一主题事件;删除事件会区分“对所有人删除”和“从我这端删除”。
- 接收宿主原始通知事件,转换字段后调用
WPP.chat.sendTextMessage()向peerPlatId发送文本消息。
- 创建和管理 WhatsApp Web
BrowserWindow。 - 每个 WhatsApp 账号使用独立
session partition。 - 注入
@wppconnect/wa-js并等待window.WPPready。 - 可通过 WhatsApp 专用 preload 暴露
window.whatsappBridge,让 WA-JS 注入脚本优先走 IPC bridge 回传事件。 - 按
accountId路由读取命令,避免多账号串号。 - 监听 WA-JS 事件并转换成统一主题事件。
- 向 Electron 主进程、渲染进程或业务系统回调派发事件。
桥接层使用 AccountRuntimeManager 管理多个账号运行时:
accounts使用Map<accountId, AccountRuntime>保存多个账号实例。- 每个
AccountRuntime可绑定独立BrowserWindow,也可绑定测试台内嵌webview的webContents。 - 每个账号使用独立
persist:whatsapp:<accountId>session partition。 - 命令和事件都按
accountId路由。
await bridge.getContacts({ accountId });
await bridge.getChats({ accountId });
await bridge.getActiveChat({ accountId });
await bridge.getActiveChatMessages({ accountId, options: { count: -1 } });
await bridge.getChatHistory({ accountId, chatId, options: { count: -1 } });
await bridge.getAllMessages({ accountId, messageOptions: { count: -1 } });
await bridge.getReadSnapshot({ accountId, messageOptions: { count: -1 } });
await bridge.installMessageTranslations({ accountId });
await bridge.installChatListTags({ accountId, label: 'AI' });
await bridge.notifySession(rawHostEvent);
bridge.setActiveAccount(accountId);setActiveAccount(accountId) 用于多账号场景下声明宿主当前正在查看的账号。单账号运行时会默认把第一个启动的账号作为 active account;宿主 UI 切换账号时应同步调用一次。
通过 IPC 使用时,对应通道是 whatsapp:set-active-account,payload 为 { accountId };startAccount / attachAccountWebContents 也支持传入 makeActive: true。
getChatHistory() 用于读取单个会话当前在 WhatsApp Web 里已经加载/可读取的消息,并按业务需要拆成文本消息和媒体消息:
const history = await bridge.getChatHistory({
accountId: 'wa-account-xxx',
chatId: '268487683498081@lid',
options: { count: -1 }
});
console.log(history.textMessages);
console.log(history.mediaMessages);返回结构:
{
chatId: '268487683498081@lid',
loadedOnly: true,
textMessages: [],
mediaMessages: [],
otherMessages: [],
totals: {
loaded: 0,
text: 0,
media: 0,
other: 0
}
}mediaMessages 包含 voice、audio、video、image、document 和 sticker,其中 document 对应文件/文档。otherMessages 用于保留位置、联系人名片、投票、系统消息、撤回消息等非文本、非媒体类型,避免读取结果静默丢失。
WhatsApp Web 的 Developer Tools Console 会直接使用中文前缀打印当前会话历史:文本消息合并为一条 [WhatsApp历史消息读取] 文本消息读取完成 日志;媒体消息会在后台异步下载/转换后合并为一条 [WhatsApp历史消息读取] 媒体消息读取完成 日志。自动读取只会在当前会话页面、WA-JS 会话接口和会话面板都加载稳定后触发;切换会话、新消息、消息 Store 变化,以及调用 getChatHistory、getMessages、getActiveChatMessages 时都会打印。
媒体消息的日志展示对象会尝试调用 WA-JS 下载媒体,并把 content.text / content.displayText / logContent 写成 data:image/...;base64,...、data:audio/...;base64,... 这类纯内容;不会在内容前拼接 [图片]、[语音] 等类型前缀。媒体下载可能需要等待 WhatsApp 文件预览可用,所以这部分始终异步执行,不会阻塞文本消息日志。如果下载失败,会保留文件名或 mimetype,并在 media.base64Error 里说明原因。
这个接口不会主动帮用户向上滚动或强制加载更早历史;它只返回 WhatsApp Web 当前已经加载出来、且 WA-JS 当前可读取的消息。用户之后如果在同一会话向上滚动加载了更多历史,再次调用同一个 getChatHistory() 就会得到更新后的已加载集合。
桥接层默认会在 WA-JS ready 后安装消息译文增强;当聊天页面加载完成并读取到当前会话历史消息后,会在 WhatsApp 页面当前可见的文本消息下方追加桥接层自己的译文节点。当前译文内容先使用原文,后续可在 DomTranslator 内把 renderTranslation() 的文本来源替换为真实翻译结果。
await bridge.getMessageTranslationsStatus({ accountId: 'wa-account-xxx' });
await bridge.installMessageTranslations({ accountId: 'wa-account-xxx' }); // 手动重新安装或刷新配置
await bridge.removeMessageTranslations({ accountId: 'wa-account-xxx' });通过 IPC 使用时,对应通道为:
whatsapp:install-message-translations
whatsapp:get-message-translations-status
whatsapp:remove-message-translations
这条链路只做非破坏性增强:不改写 WhatsApp 原消息气泡内容,不删除内部节点,不依赖动态 class 或深层 CSS 路径。它优先读取可见消息上的 data-pre-plain-text 和附近 data-id,并使用 MutationObserver 在 WhatsApp Web 重绘后重新应用。由于 WhatsApp Web 会虚拟滚动,只有当前已经渲染到 DOM 中的消息会显示译文;向上滚动加载更多历史后会自动尝试补齐可见消息。
桥接层默认会在 WA-JS ready 后安装会话列表增强:在 WhatsApp 左侧可见会话列表的名称后面追加一个 AI tag。该增强只插入 data-wa-bridge-chat-list-tag="true" 的旁路节点,不改写 WhatsApp 原始名称;列表虚拟滚动或重绘后会通过 MutationObserver 自动补回。
await bridge.getChatListTagsStatus({ accountId: 'wa-account-xxx' });
await bridge.installChatListTags({ accountId: 'wa-account-xxx', label: 'AI' }); // 手动重新安装或修改 tag 文案
await bridge.removeChatListTags({ accountId: 'wa-account-xxx' });通过 IPC 使用时,对应通道为:
whatsapp:install-chat-list-tags
whatsapp:get-chat-list-tags-status
whatsapp:remove-chat-list-tags
getConnectionState(accountId) 会返回当前 WhatsApp 登录状态、stream 信息和登录账号资料。bridge 会主动从 WA-JS 可用的账号资料接口里读取当前登录账号头像,并放到 Social Hub 可识别的 account.profilePicUrl 字段:
{
isAuthenticated: true,
isReady: true,
streamData: {
mode: 'MAIN',
info: 'NORMAL'
},
account: {
id: '8613xxxx@c.us',
realAccountId: '8613xxxx@c.us',
platformUserId: '8613xxxx@c.us',
wid: '8613xxxx@c.us',
me: '8613xxxx@c.us',
phoneNumber: '8613xxxx',
pushName: 'Display Name',
name: 'Display Name',
profilePicUrl: 'data:image/jpeg;base64,...'
}
}头像解析会兼容 WA-JS / WhatsApp Web 中常见的 profilePicUrl、avatar、avatarUrl、pictureUrl、imgUrl、img、imgFull、eurl 等字段;这些字段出现在 root,或嵌套在 account、profile、user、me、currentUser、currentAccount、accountInfo、userInfo、_data 里时都会被尝试读取。
WhatsApp 的 media-*.cdn.whatsapp.net 头像地址是临时媒体 URL,可能会触发下载或不可由宿主直接展示。bridge 不会把这类原始 CDN URL 原样放进 account.profilePicUrl;如果 WA-JS 可下载图片,会转换成 data:image/...,否则返回 null。
whatsapp.account.status
whatsapp.account.qr
whatsapp.contacts.synced
whatsapp.chats.synced
whatsapp.messages.synced
whatsapp.message.inbound
whatsapp.message.ack
whatsapp.message.deleted
whatsapp.message.edited
whatsapp.chat.updated
whatsapp.unread.updated
whatsapp.dom.translation.installed
whatsapp.dom.translation.error
whatsapp.dom.chat_list_tags.installed
whatsapp.dom.chat_list_tags.error
whatsapp.host_notification.ack
whatsapp.error
whatsapp.health
whatsapp.account.status 的 data.status 对齐 social_hub 的统一平台账号状态契约,当前可能值:
online
offline
loading
needs_login
qr
invalid
error
data.state 和 data.rawState 保留 bridge 内部状态机的原始状态名,仅用于调试和向后兼容。宿主项目应只按 data.status 做业务判断,不需要识别 ready、injecting、stopping、render-process-gone 等内部状态。
包入口同时导出 PLATFORM_ACCOUNT_STATUSES,用于需要复用允许值的宿主或测试代码。
- WhatsApp 页面 DOM 修改与插入方案:
docs/whatsapp-dom-injection-sharing.md
const { BrowserWindow, ipcMain } = require('electron');
const {
WhatsAppPlatformBridge,
registerWhatsappIpc,
resolveWhatsappPreloadPath
} = require('whatsapp-platform-bridge');
const bridge = new WhatsAppPlatformBridge({
defaultShowWindow: true,
businessCallbackUrl: process.env.WHATSAPP_CALLBACK_URL
});
registerWhatsappIpc({ bridge, ipcMain, BrowserWindow });
bridge.on('event', (event) => {
console.log(event.topic, event.accountId, event.traceId);
});桥接层会在 WhatsApp Web 发送任意非空文本消息、以及发送媒体时填写的任意非空文本说明前触发 whatsapp.message.before_send,并在页面 console 打印一条 [WhatsApp发送拦截] before_send。这里按消息类型拦截,不按关键词筛选;只有真正进入发送前决策时才会打印,进入页面、输入框聚焦、普通点击不会触发。
当前内置策略默认返回 { action: 'block' },因此会先阻止所有非空文本消息和媒体文本说明发送。即使页面暂时拿不到主进程 before-send 决策通道,也会走 fallback block。后续业务规则应插入 AccountRuntime.handleBeforeSend() 中:返回 { action: 'allow' } 时继续发送;返回 { action: 'modify', text } 时会先改写输入框/消息对象再继续发送;返回 { action: 'block' } 时页面侧不会重放原始发送事件。
如果由桥接层自己创建 BrowserWindow,它会自动使用 WhatsApp preload。
如果宿主项目已经有自己的 WebContentsView / webview,需要在加载 WhatsApp Web 前把 preload 设置到目标页面:
const preload = resolveWhatsappPreloadPath();
const view = new WebContentsView({
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
});
await bridge.attachAccountWebContents({
accountId,
webContents: view.webContents,
userAgent
});事件回传链路优先级:
- WhatsApp preload 暴露
window.whatsappBridge。 - WA-JS 注入脚本调用
pushMessage、pushChat、pushStatus、pushUnread或pushEvent。 - 主进程
registerWhatsappIpc()接收whatsapp:push-*IPC,并校验事件来源是否是已绑定账号的webContents。 - 如果页面没有安装 preload,则回退到旧的
console-message前缀回传,方便继续使用现有 Electron Tester。
登录完成且页面加载完成后,注入脚本会主动读取 WPP.chat.list() 并推送 whatsapp.chats.synced,payload 中包含 realAccountId / platformUserId、标准化后的 chats,以及已经按宿主上传接口 /api/chat/desk/chatConversation/up 整理好的 conversations。会话列表会为私聊和群聊尝试补齐 profilePicUrl,并优先转换成 data:image/...;base64,...;WhatsApp 临时 CDN 头像地址如果无法转换会返回 null,避免宿主直接展示过期媒体 URL。每个会话也会尽量补齐 lastMessage;媒体最后消息会在 lastMessage.content.text / displayText 中按类型显示为 [图片]、[视频]、[贴纸] 等。如果 ready 时聊天列表尚未水合,会短暂重试;宿主也可以继续主动调用 getChats({ accountId }) 获取同一结构。
whatsapp.message.inbound 会推送当前 active account 下所有新消息,不再限制为 WhatsApp Web 当前打开的 active chat。新会话、新消息、会话集合变化、联系人名和群名称变化会整理成批量 whatsapp.chat.updated 事件,结构与 whatsapp.chats.synced 一致;宿主层可直接读取 event.data.conversations 调用会话上传接口。消息 Store 普通变化、历史消息加载、标签和未读数变化不会触发会话增量上传;账号级未读数继续通过下面的 whatsapp.unread.updated 反映。多账号场景仍按 activeAccountId 抑制后台账号的详细消息事件。
whatsapp.messages.synced 历史消息会在每次进入会话时重新读取并传输,不依赖本地已同步缓存。若读取完成时宿主 businessCallbackUrl 暂时不可用,桥接层会把该历史消息事件放入内存待投递队列,并在宿主连接恢复或账号 ready/online 后继续重试;默认每 2 秒重试一次,最多保留 5 分钟或 100 条待投递事件。可通过 businessCallbackRetryDelayMs、businessCallbackQueueTtlMs、businessCallbackQueueLimit 调整。该队列只用于等待宿主通信连接,不会持久化为本地历史缓存。
whatsapp.unread.updated 会输出账号级总未读数,而不是单个 chat 的增量。事件 data 结构如下:
{
accountId: 'wa-account-xxx',
realAccountId: '8613xxxx@c.us',
platform: 'whatsapp',
action: 'set',
unreadCount: 12,
timestamp: 1777272000000,
sourceEvent: 'chat.unread_count_changed'
}宿主层要把后台通知投递到指定客服会话时,只需要把原始 MQTT / 业务事件传给 notifySession(rawHostEvent) 或 IPC whatsapp:notify-session。字段转换由桥接层负责:tempPlatMessageId 会转换为 requestId,uid 会转换为 accountKey,topic 里的 /desk/message/{deskId} 会转换为 deskId,原始 source 会保留为 originalSource,标准语义固定为 source: "host-notify"、deliveryMode: "notify-session"。
默认实现会用归一化后的 accountId/accountKey/platId 找到运行中的 WhatsApp 账号,并调用 WPP.chat.sendTextMessage(peerPlatId, messageContent, { waitForAck: true, markIsRead: true }) 真实发送文本。桥接层会输出 whatsapp.host_notification.ack 回执;发送成功时 status: "delivered",发送失败时 status: "failed" 并带 reason 和 error。
详见:docs/host-session-notification.md。
npm install
npm run check注意:WA-JS 依赖 WhatsApp Web 内部实现。WhatsApp Web 更新后,注入、事件名或 API 行为可能需要同步适配。