Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WhatsApp Platform Bridge

这是 WhatsApp 平台桥接项目,负责在 Electron 主进程中管理 WhatsApp Web 多账号实例,并通过 @wppconnect/wa-js 完成 WhatsApp 消息读取和宿主通知文本发送。

当前范围

当前阶段提供数据读取,以及宿主层通知到 WhatsApp 文本消息的发送能力:

  • 多账号登录和多开。
  • 获取联系人列表。
  • 获取会话列表。
  • 获取当前 WhatsApp 选中会话的名称和消息。
  • 获取全部已加载/可读取消息。
  • 标准化消息类型、来源、去向、所属账号和所属会话。
  • 监听新入站消息,并转成统一主题事件。
  • 监听单条消息删除与编辑事件,并转成统一主题事件;删除事件会区分“对所有人删除”和“从我这端删除”。
  • 接收宿主原始通知事件,转换字段后调用 WPP.chat.sendTextMessage()peerPlatId 发送文本消息。

职责边界

  • 创建和管理 WhatsApp Web BrowserWindow
  • 每个 WhatsApp 账号使用独立 session partition
  • 注入 @wppconnect/wa-js 并等待 window.WPP ready。
  • 可通过 WhatsApp 专用 preload 暴露 window.whatsappBridge,让 WA-JS 注入脚本优先走 IPC bridge 回传事件。
  • accountId 路由读取命令,避免多账号串号。
  • 监听 WA-JS 事件并转换成统一主题事件。
  • 向 Electron 主进程、渲染进程或业务系统回调派发事件。

多账号同时在线

桥接层使用 AccountRuntimeManager 管理多个账号运行时:

  • accounts 使用 Map<accountId, AccountRuntime> 保存多个账号实例。
  • 每个 AccountRuntime 可绑定独立 BrowserWindow,也可绑定测试台内嵌 webviewwebContents
  • 每个账号使用独立 persist:whatsapp:<accountId> session partition。
  • 命令和事件都按 accountId 路由。

主进程 API

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 包含 voiceaudiovideoimagedocumentsticker,其中 document 对应文件/文档。otherMessages 用于保留位置、联系人名片、投票、系统消息、撤回消息等非文本、非媒体类型,避免读取结果静默丢失。

WhatsApp Web 的 Developer Tools Console 会直接使用中文前缀打印当前会话历史:文本消息合并为一条 [WhatsApp历史消息读取] 文本消息读取完成 日志;媒体消息会在后台异步下载/转换后合并为一条 [WhatsApp历史消息读取] 媒体消息读取完成 日志。自动读取只会在当前会话页面、WA-JS 会话接口和会话面板都加载稳定后触发;切换会话、新消息、消息 Store 变化,以及调用 getChatHistorygetMessagesgetActiveChatMessages 时都会打印。

媒体消息的日志展示对象会尝试调用 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 中的消息会显示译文;向上滚动加载更多历史后会自动尝试补齐可见消息。

会话列表私聊 AI 标记

桥接层默认会在 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 中常见的 profilePicUrlavataravatarUrlpictureUrlimgUrlimgimgFulleurl 等字段;这些字段出现在 root,或嵌套在 accountprofileusermecurrentUsercurrentAccountaccountInfouserInfo_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.statusdata.status 对齐 social_hub 的统一平台账号状态契约,当前可能值:

online
offline
loading
needs_login
qr
invalid
error

data.statedata.rawState 保留 bridge 内部状态机的原始状态名,仅用于调试和向后兼容。宿主项目应只按 data.status 做业务判断,不需要识别 readyinjectingstoppingrender-process-gone 等内部状态。

包入口同时导出 PLATFORM_ACCOUNT_STATUSES,用于需要复用允许值的宿主或测试代码。

技术分享

  • WhatsApp 页面 DOM 修改与插入方案:docs/whatsapp-dom-injection-sharing.md

在 Electron 主进程中使用

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
});

事件回传链路优先级:

  1. WhatsApp preload 暴露 window.whatsappBridge
  2. WA-JS 注入脚本调用 pushMessagepushChatpushStatuspushUnreadpushEvent
  3. 主进程 registerWhatsappIpc() 接收 whatsapp:push-* IPC,并校验事件来源是否是已绑定账号的 webContents
  4. 如果页面没有安装 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 条待投递事件。可通过 businessCallbackRetryDelayMsbusinessCallbackQueueTtlMsbusinessCallbackQueueLimit 调整。该队列只用于等待宿主通信连接,不会持久化为本地历史缓存。

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 会转换为 requestIduid 会转换为 accountKeytopic 里的 /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" 并带 reasonerror

详见:docs/host-session-notification.md

运行检查

npm install
npm run check

注意:WA-JS 依赖 WhatsApp Web 内部实现。WhatsApp Web 更新后,注入、事件名或 API 行为可能需要同步适配。

About

个人使用

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages