macOS 快速开始

本页用 6 步完成 macOS 桌面端接入:安装 SDK(当前仅本地 file:)→ 主进程初始化(IMLib + 本地 SQLite)→ 注册 IPC 桥 → 渲染进程调用 preload 桥 → 连接并收发 → 打包成可双击的 .app。示例以仓库内 sdk/electron 的实际导出为准(103 个语义操作、16 类事件)。

  1. 准备环境

    macOS 11+、Node 22+、Xcode 命令行工具;IM 服务端已启动(示例地址,本地联调替换为实际地址)。

  2. 安装 SDK(本地包)

    当前 SDK **未发布到 npm**(`@0.1.0` 会 404),从仓库本地安装:`npm install @ni-paas-im/electron@file:../sdk/electron electron@35`。

  3. 主进程:初始化 + 注册 IPC

    `initService({ appkey, serverUrl, dbPath })` 返回 `{ client, destroy, capabilities, ... }`;`registerIpc({ ipcMain, service })` 注册 `ni-paas-im:*` 通道。

  4. 渲染进程:使用 preload 桥

    窗口 `sandbox:false`(ESM preload 必需),`contextIsolation:true`;页面通过 `window.niPaasIMBridge.imlib.*` 调用,并用 `onEvent`/`onState` 订阅。

  5. 连接并收发

    `connect(token)` → 收到 `CONNECTED`;`getConversationList()` 读本地库;`sendTextMessage(1, targetId, text)` 发送。

  6. 运行与打包

    `npx electron .` 开发态启动;`npx electron-builder --mac` 产出 `.app` / `.dmg`(见「打包」页)。

1. 准备环境​

Shell
sw_vers                  # macOS 11+
node -v                  # v22.5+
xcode-select -p          # 已安装 Command Line Tools
curl -s "$SERVER/api/__contract/introspect" | head -c 120   # 服务端可达性

2. 安装 SDK(本地包)​

Shell
mkdir my-desktop-app && cd my-desktop-app
npm init -y

# 当前未发布到 npm(@0.1.0 远程安装 404),只能本地 file: 安装
npm install @ni-paas-im/electron@file:../sdk/electron electron@35
npm install -D electron-builder@24

# 若目录层级不同,把 ../sdk/electron 换成你的 SDK 绝对路径
包名与安装方式
@ni-paas-im/electron-macos 不存在;SDK 包名是 @ni-paas-im/electron(版本 0.1.0),但**当前未发布到 npm**,必须用 file: 本地安装。入口:@ni-paas-im/electron/main(主进程)、@ni-paas-im/electron/main/ipc(IPC 注册)、@ni-paas-im/electron/preload(preload 桥)。
Electron 二进制安装失败?先判定再兜底

判定:npx electron --version 报 Electron failed to install correctly,或 node_modules/electron/dist 只有 LICENSE/version 而没有 Electron.app。

npm 11 / Node 26 可能拦截 postinstall,且 node node_modules/electron/install.js 可能 exit 0 但未解压;请按下面的缓存 zip 兜底(不要只“删除重装”)。

Shell
# 1) 先尝试前台执行 postinstall
npm install --foreground-scripts electron@35

# 2) 判定二进制是否存在
test -x node_modules/electron/dist/Electron.app/Contents/MacOS/Electron && npx electron --version

# 3) 缓存 zip 手工兜底(npm 静默失败时最可靠)
CACHE="${ELECTRON_CACHE:-$HOME/Library/Caches/electron}"
ZIP=$(find "$CACHE" -name 'electron-v*-darwin-*.zip' 2>/dev/null | sort | tail -1)
test -n "$ZIP" || { echo "未找到缓存 zip:先设置 ELECTRON_MIRROR 后重装"; exit 1; }
rm -rf node_modules/electron/dist
mkdir -p node_modules/electron/dist
unzip -q -o "$ZIP" -d node_modules/electron/dist
printf 'Electron.app/Contents/MacOS/Electron' > node_modules/electron/path.txt

# 4) 验证(应输出版本号,如 v35.7.5)
npx electron --version

# 5) 仍失败:镜像 + 清缓存重下
export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
export ELECTRON_CACHE="$HOME/Library/Caches/electron"
rm -rf node_modules/electron && npm install --foreground-scripts electron@35
版本兼容建议

优先使用 Node 22 LTS + npm 10(Electron 35 的安装脚本在该组合下最稳定)。Node 26 / npm 11 若仍拦截,上面的缓存 zip 兜底可直接完成安装;ELECTRON_CACHE 可指向团队共享缓存目录。

3. 主进程:初始化 + 注册 IPC​

JavaScript
// main.mjs
import { app, BrowserWindow, ipcMain } from 'electron';
import { join } from 'node:path';
import { createRequire } from 'node:module';
import { initService } from '@ni-paas-im/electron/main';
import { registerIpc } from '@ni-paas-im/electron/main/ipc';

const require = createRequire(import.meta.url);
let service;
let bridge;

app.whenReady().then(async () => {
  service = initService({
    appkey: 'demo',
    serverUrl: 'https://im.example.com',       // 本地联调替换为 http://127.0.0.1:18081
    dbPath: app.getPath('userData'),           // 目录!库文件:<dbPath>/im-electron/<appkey>/<userId>.db
  });

  // SDK 注册 ni-paas-im:invoke / subscribe / event / state 等通道
  bridge = registerIpc({ ipcMain, service });

  const win = new BrowserWindow({
    width: 1280,
    height: 800,
    webPreferences: {
      // 使用 SDK 自带 preload(不要自己写第二套通道)
      preload: join(require.resolve('@ni-paas-im/electron/package.json'), '..', 'src', 'preload', 'index.mjs'),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: false,                          // ESM preload 要求非沙箱;页面仍无 Node 访问
    },
  });
  await win.loadFile('renderer/index.html');
});

app.on('window-all-closed', () => {
  if (bridge) bridge.dispose();
  if (service) service.destroy();
  if (process.platform !== 'darwin') app.quit();
});
registerIpc 接线契约

唯一推荐写法:registerIpc({ ipcMain, service }),service 必须是 initService() 返回的**整个包装对象**,不要自己拆包。

包装对象形如 { client, destroy, getDeviceId, getServerTime, setEventSink, setStateSink, getState, capabilities };102 个 IMLib 操作与 localDb 命名空间都在 service.client 上,registerIpc 内部按 service.client ?? service 解析 dispatch 目标。

也支持直接传裸 client(new IMElectronClient({...}))作为 service;两种接法都已覆盖回归测试。事件 / 状态 sink 由 registerIpc 绑定到包装对象,宿主不用手动调。

旧版本按错误接法接线时,renderer 的所有操作会返回 3004 SDK_UNSUPPORTED(错误文案会提示 dispatch 目标未解析);该问题已修复,若仍遇到请确认 SDK 版本并保持传包装对象。

4. 渲染进程:使用 preload 桥​

JavaScript
// renderer/app.mjs —— 页面只看到 window.niPaasIMBridge(无需自己写 contextBridge)
const bridge = window.niPaasIMBridge;
const im = bridge.imlib;

// 连接
const res = await im.connect(TOKEN);
if (res.code !== 0) console.error('connect failed', res.code, res.msg);

// 订阅事件:{ event, payload, seq, ts }
const offEvent = bridge.onEvent(({ event, payload }) => {
  if (event === 'MESSAGES') renderMessages(payload.messages);
  if (event === 'CONVERSATION') refreshConversations();
});

// 订阅连接状态:{ status, userId, dbOpen }
const offState = bridge.onState((state) => updateStatus(state));

// 本地库(主进程 SQLite)
const conversations = await im.getConversationList();
const history = await im.getHistoryMessages(1, 'user2', 50, 0);

// 发送文本(三个位置参数:conversationType, targetId, text)
const sent = await im.sendTextMessage(1, 'user2', '你好,NI PaaS');
if (sent.code !== 0) console.error('send failed', sent.code, sent.msg);
桥的能力边界

页面只能调用白名单方法(imlib.*、localDb.*)以及 onEvent / onState / capabilities,不存在自由拼接的 ipcRenderer.invoke。

addEventListener / removeEventListener 是渲染进程本地实现(onEvent 返回取消函数),不走 IPC。

5. 连接并收发​

Shell
# 1) 启动主进程(开发态)
npx electron .

# 2) 用 Node 脚本或第二个客户端给目标用户发一条消息,验证收到
#    无效 Token 会返回 1002 INVALID_TOKEN(msg 为服务端原文,如 invalid token)
# 3) 日志中应看到 CONNECTED → MESSAGES → sendTextMessage code=0
两个账号验证

同一进程内只允许一个登录态;验证收发请开两个窗口/两个进程(或用 Node 脚本作为对端)各登录一个用户。

连接失败先看错误码

1002 INVALID_TOKEN(无效/过期/签名不对)或 1003 FORBIDDEN:不会自动重连,提示重新登录并重新签发 Token。

3001(传输层)/ 3002(超时):SDK 会自动重连;先检查服务端地址、网络与代理,UI 显示「网络异常,正在重连」。

主进程日志会打印 [im-electron] connect failed: code=… msg=… serverUrl=… ws=…,可直接用于定位。详见 错误码。

6. 验收证据(截图 / 录屏)​

Shell
# 主进程里用 webContents.capturePage() 抓窗口帧(真实渲染)
# 连续抓帧后用 AVFoundation/ffmpeg 编码为 mp4;参考:
#   const image = await win.webContents.capturePage();
#   fs.writeFileSync('frame-0001.png', image.toPNG());
# 或系统级:screencapture -x -t png shot.png(整屏)

下一步​